From d8f20d2e667ee4104fb6c4b9e5e23fce73f44b5a Mon Sep 17 00:00:00 2001 From: pvdk Date: Sat, 7 Dec 2013 16:17:07 +0100 Subject: [PATCH 001/303] Added the infrastructure to start working on the installation wizard --- CMakeLists.txt | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f6014dff6e..f5e2d0a818 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ option(BUILD_ESMTOOL "build ESM inspector" ON) option(BUILD_LAUNCHER "build Launcher" ON) option(BUILD_MWINIIMPORTER "build MWiniImporter" ON) option(BUILD_OPENCS "build OpenMW Construction Set" ON) +option(BUILD_WIZARD "build Installation Wizard" ON) option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF) option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest ang GMock frameworks" OFF) @@ -384,6 +385,10 @@ IF(NOT WIN32 AND NOT APPLE) IF(BUILD_OPENCS) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/opencs" DESTINATION "${BINDIR}" ) ENDIF(BUILD_OPENCS) + IF(BUILD_WIZARD) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/wizard" DESTINATION "${BINDIR}" ) + ENDIF(BUILD_WIZARD) + # Install licenses INSTALL(FILES "DejaVu Font License.txt" DESTINATION "${LICDIR}" ) @@ -434,7 +439,7 @@ IF(NOT WIN32 AND NOT APPLE) Data files from the original game is required to run it.") SET(CPACK_DEBIAN_PACKAGE_NAME "openmw") SET(CPACK_DEBIAN_PACKAGE_VERSION "${VERSION_STRING}") - SET(CPACK_PACKAGE_EXECUTABLES "openmw;OpenMW opencs;OpenCS bsatool;Bsatool esmtool;Esmtool omwlauncher;OMWLauncher mwiniimporter;MWiniImporter") + SET(CPACK_PACKAGE_EXECUTABLES "openmw;OpenMW opencs;OpenCS bsatool;Bsatool esmtool;Esmtool omwlauncher;OMWLauncher mwiniimporter;MWiniImporter openmw-wizard;OpenMW Wizard") SET(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.11.2), libfreetype6 (>= 2.2.1), libgcc1 (>= 1:4.1.1), libmpg123-0 (>= 1.12.1), libopenal1 (>= 1:1.12.854), libsndfile1 (>= 1.0.23), libstdc++6 (>= 4.4.5), libuuid1 (>= 2.17.2), libqtgui4 (>= 4.7.0)") SET(CPACK_DEBIAN_PACKAGE_SECTION "Games") @@ -476,6 +481,10 @@ if(WIN32) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/opencs.exe" DESTINATION ".") INSTALL(FILES "${OpenMW_BINARY_DIR}/opencs.cfg" DESTINATION ".") ENDIF(BUILD_OPENCS) + IF(BUILD_WIZARD) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/openmw-wizard.exe" DESTINATION ".") + ENDIF(BUILD_WIZARD) + INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION ".") @@ -493,6 +502,9 @@ if(WIN32) IF(BUILD_OPENCS) SET(CPACK_PACKAGE_EXECUTABLES "${CPACK_PACKAGE_EXECUTABLES};opencs;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 @@ -573,6 +585,10 @@ if (BUILD_OPENCS) add_subdirectory (apps/opencs) endif() +if (BUILD_WIZARD) + add_subdirectory(apps/wizard) +endif() + # UnitTests if (BUILD_UNITTESTS) add_subdirectory( apps/openmw_test_suite ) @@ -649,6 +665,9 @@ if (WIN32) if (BUILD_ESMTOOL) set_target_properties(esmtool PROPERTIES COMPILE_FLAGS ${WARNINGS}) endif (BUILD_ESMTOOL) + if (BUILD_WIZARD) + set_target_properties(openmw-wizard PROPERTIES COMPILE_FLAGS ${WARNINGS}) + endif (BUILD_WIZARD) endif(MSVC) # Same for MinGW From f458f0c6c16d13bac0d45ccd7ed972d92cfed870 Mon Sep 17 00:00:00 2001 From: pvdk Date: Sat, 7 Dec 2013 23:17:50 +0100 Subject: [PATCH 002/303] Added the Wizard .ui/resource files and some basic source to get started --- apps/wizard/CMakeLists.txt | 96 +++++++++++ apps/wizard/main.cpp | 51 ++++++ apps/wizard/wizard.cpp | 1 + apps/wizard/wizard.hpp | 1 + files/ui/wizard/componentselectionpage.ui | 60 +++++++ files/ui/wizard/conclusionpage.ui | 36 ++++ files/ui/wizard/configurationpage.ui | 58 +++++++ files/ui/wizard/existinginstallationpage.ui | 85 ++++++++++ files/ui/wizard/importpage.ui | 67 ++++++++ files/ui/wizard/installationpage.ui | 51 ++++++ files/ui/wizard/installationtargetpage.ui | 99 +++++++++++ files/ui/wizard/intropage.ui | 34 ++++ files/ui/wizard/methodselectionpage.ui | 156 ++++++++++++++++++ files/wizard/icons/tango/48x48/folder.png | Bin 0 -> 1610 bytes .../icons/tango/48x48/system-installer.png | Bin 0 -> 2663 bytes files/wizard/icons/tango/index.theme | 8 + files/wizard/wizard.qrc | 7 + 17 files changed, 810 insertions(+) create mode 100644 apps/wizard/CMakeLists.txt create mode 100644 apps/wizard/main.cpp create mode 100644 apps/wizard/wizard.cpp create mode 100644 apps/wizard/wizard.hpp create mode 100644 files/ui/wizard/componentselectionpage.ui create mode 100644 files/ui/wizard/conclusionpage.ui create mode 100644 files/ui/wizard/configurationpage.ui create mode 100644 files/ui/wizard/existinginstallationpage.ui create mode 100644 files/ui/wizard/importpage.ui create mode 100644 files/ui/wizard/installationpage.ui create mode 100644 files/ui/wizard/installationtargetpage.ui create mode 100644 files/ui/wizard/intropage.ui create mode 100644 files/ui/wizard/methodselectionpage.ui create mode 100644 files/wizard/icons/tango/48x48/folder.png create mode 100644 files/wizard/icons/tango/48x48/system-installer.png create mode 100644 files/wizard/icons/tango/index.theme create mode 100644 files/wizard/wizard.qrc diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt new file mode 100644 index 0000000000..d6423daad4 --- /dev/null +++ b/apps/wizard/CMakeLists.txt @@ -0,0 +1,96 @@ +set(WIZARD + main.cpp + wizard.cpp + +) +# if(NOT WIN32) +# LIST(APPEND WIZARD unshieldthread.cpp) +# endif(NOT WIN32) + +set(WIZARD_HEADER + wizard.hpp +) +# if(NOT WIN32) +# LIST(APPEND WIZARD_HEADER unshieldthread.hpp) +# endif(NOT WIN32) + + +# Headers that must be pre-processed +set(WIZARD_HEADER_MOC + wizard.hpp +) + +# if(NOT WIN32) +# LIST(APPEND WIZARD_HEADER_MOC unshieldthread.hpp) +# endif(NOT WIN32) + + +set(WIZARD_UI + ${CMAKE_SOURCE_DIR}/files/ui/wizard/componentselectionpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/conclusionpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/configurationpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/existinginstallationpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/importpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationtargetpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/intropage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/methodselectionpage.ui +) + +source_group(wizard FILES ${WIZARD} ${WIZARD_HEADER}) + +find_package(Qt4 REQUIRED) +set(QT_USE_QTGUI 1) + +# Set some platform specific settings +if(WIN32) + set(GUI_TYPE WIN32) + set(QT_USE_QTMAIN TRUE) +endif(WIN32) + +QT4_ADD_RESOURCES(RCC_SRCS ${CMAKE_SOURCE_DIR}/files/wizard/wizard.qrc) +QT4_WRAP_CPP(MOC_SRCS ${WIZARD_HEADER_MOC}) +QT4_WRAP_UI(UI_HDRS ${WIZARD_UI}) + + +include(${QT_USE_FILE}) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) +if(NOT WIN32) + include_directories(${LIBUNSHIELD_INCLUDE}) +endif(NOT WIN32) + +add_executable(openmw-wizard + ${GUI_TYPE} + ${WIZARD} + ${WIZARD_HEADER} + ${RCC_SRCS} + ${MOC_SRCS} + ${UI_HDRS} +) + +target_link_libraries(openmw-wizard +# ${Boost_LIBRARIES} + ${QT_LIBRARIES} +) +if(NOT WIN32) + target_link_libraries(openmw-wizard + ${LIBUNSHIELD_LIBRARY} + ) +endif(NOT WIN32) + + + +if(DPKG_PROGRAM) + INSTALL(TARGETS openmw-wizard RUNTIME DESTINATION games COMPONENT openmw-wizard) +endif() + +if (BUILD_WITH_CODE_COVERAGE) + add_definitions (--coverage) + target_link_libraries(openmw-wizard gcov) +endif() + +# Workaround for binutil => 2.23 problem when linking, should be fixed eventually upstream +if (UNIX AND NOT APPLE) +target_link_libraries(openmw-wizard dl Xt) +endif() + diff --git a/apps/wizard/main.cpp b/apps/wizard/main.cpp new file mode 100644 index 0000000000..ed3f3a6eae --- /dev/null +++ b/apps/wizard/main.cpp @@ -0,0 +1,51 @@ +#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 + +int main(int argc, char *argv[]) +{ + + QApplication app(argc, argv); + + // Now we make sure the current dir is set to application path + QDir dir(QCoreApplication::applicationDirPath()); + + #ifdef Q_OS_MAC + if (dir.dirName() == "MacOS") { + dir.cdUp(); + dir.cdUp(); + dir.cdUp(); + } + + // force Qt to load only LOCAL plugins, don't touch system Qt installation + QDir pluginsPath(QCoreApplication::applicationDirPath()); + pluginsPath.cdUp(); + pluginsPath.cd("Plugins"); + + QStringList libraryPaths; + libraryPaths << pluginsPath.path() << QCoreApplication::applicationDirPath(); + app.setLibraryPaths(libraryPaths); + #endif + + QDir::setCurrent(dir.absolutePath()); + + // Support non-latin characters + QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8")); + +// Launcher::MainDialog mainWin; + +// if (mainWin.setup()) { +// mainWin.show(); +// } else { +// return 0; +// } + + return app.exec(); +} diff --git a/apps/wizard/wizard.cpp b/apps/wizard/wizard.cpp new file mode 100644 index 0000000000..8d1c8b69c3 --- /dev/null +++ b/apps/wizard/wizard.cpp @@ -0,0 +1 @@ + diff --git a/apps/wizard/wizard.hpp b/apps/wizard/wizard.hpp new file mode 100644 index 0000000000..8d1c8b69c3 --- /dev/null +++ b/apps/wizard/wizard.hpp @@ -0,0 +1 @@ + diff --git a/files/ui/wizard/componentselectionpage.ui b/files/ui/wizard/componentselectionpage.ui new file mode 100644 index 0000000000..c3333752ad --- /dev/null +++ b/files/ui/wizard/componentselectionpage.ui @@ -0,0 +1,60 @@ + + + ComponentSelectionPage + + + + 0 + 0 + 454 + 393 + + + + WizardPage + + + Select Components + + + Which components should be installed? + + + + + + <html><head/><body><p>Select which official Morrowind expansions should be installed. For best results, it is recommended to have both expansions installed.</p><p><span style=" font-weight:bold;">Note:</span> It is possible to install expansions later by re-running this Wizard.<br/></p></body></html> + + + true + + + + + + + Selected components: + + + + + + + + + + Qt::Vertical + + + + 20 + 81 + + + + + + + + + diff --git a/files/ui/wizard/conclusionpage.ui b/files/ui/wizard/conclusionpage.ui new file mode 100644 index 0000000000..bedad3e2aa --- /dev/null +++ b/files/ui/wizard/conclusionpage.ui @@ -0,0 +1,36 @@ + + + ConclusionPage + + + + 0 + 0 + 400 + 300 + + + + WizardPage + + + Completing the OpenMW Wizard + + + + + + The OpenMW Wizard successfully installed Morrowind on your computer. + +Click Finish to close the Wizard. + + + true + + + + + + + + diff --git a/files/ui/wizard/configurationpage.ui b/files/ui/wizard/configurationpage.ui new file mode 100644 index 0000000000..8340b2ebf9 --- /dev/null +++ b/files/ui/wizard/configurationpage.ui @@ -0,0 +1,58 @@ + + + WizardPage + + + + 0 + 0 + 440 + 300 + + + + WizardPage + + + + + + <html><head/><body><p>OpenMW needs to import settings from the Morrowind configuration file in order to function properly. Select the steps you want to be taken and click Next when you are ready to continue.</p><p><span style=" font-weight:600;">Note:</span> it is possible to import settings later, by re-running this Wizard.</p><p/></body></html> + + + true + + + + + + + Import settings from Morrowind.ini + + + + + + + Import selected add-ons (creates a new Cotent List in the launcher) + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/files/ui/wizard/existinginstallationpage.ui b/files/ui/wizard/existinginstallationpage.ui new file mode 100644 index 0000000000..579ddbe672 --- /dev/null +++ b/files/ui/wizard/existinginstallationpage.ui @@ -0,0 +1,85 @@ + + + ExistingInstallationPage + + + + 0 + 0 + 400 + 300 + + + + WizardPage + + + Select Existing Installation + + + Select an existing installation for OpenMW to use or modify. + + + + + + Detected Installations: + + + + + + + + No existing installations detected + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Browse... + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/files/ui/wizard/importpage.ui b/files/ui/wizard/importpage.ui new file mode 100644 index 0000000000..21037f3e8b --- /dev/null +++ b/files/ui/wizard/importpage.ui @@ -0,0 +1,67 @@ + + + ImportPage + + + + 0 + 0 + 513 + 328 + + + + WizardPage + + + Import Settings + + + Import settings from the Morrowind installation. + + + + + + <html><head/><body><p>OpenMW needs to import settings from the Morrowind configuration file in order to function properly.</p><p><span style=" font-weight:bold;">Note:</span> It is possible to import settings later by re-running this Wizard.</p><p/></body></html> + + + true + + + + + + + Import settings from Morrowind.ini + + + true + + + + + + + Import previously selected add-ons (creates a new Content List in the launcher) + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/files/ui/wizard/installationpage.ui b/files/ui/wizard/installationpage.ui new file mode 100644 index 0000000000..3e94c1e30b --- /dev/null +++ b/files/ui/wizard/installationpage.ui @@ -0,0 +1,51 @@ + + + InstallationPage + + + + 0 + 0 + 518 + 423 + + + + WizardPage + + + Installing + + + Please wait while Morrowind is installed on your computer. + + + + + + Extracting: %1 + + + + + + + 24 + + + + + + + false + + + true + + + + + + + + diff --git a/files/ui/wizard/installationtargetpage.ui b/files/ui/wizard/installationtargetpage.ui new file mode 100644 index 0000000000..6bdac55927 --- /dev/null +++ b/files/ui/wizard/installationtargetpage.ui @@ -0,0 +1,99 @@ + + + InstallationTargetPage + + + + 0 + 0 + 400 + 300 + + + + WizardPage + + + Select Installation Destination + + + Where should Morrowind be installed? + + + + + + + + + 0 + 0 + + + + <html><head/><body><p><img src=":/icons/tango/48x48/folder.png"/></p></body></html> + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Morrowind will be installed to the following location. + + + true + + + + + + + + + Click Next to continue. If you would like to select a different location, click Browse. + + + true + + + + + + + + + + + + Browse... + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + diff --git a/files/ui/wizard/intropage.ui b/files/ui/wizard/intropage.ui new file mode 100644 index 0000000000..bb597e3a61 --- /dev/null +++ b/files/ui/wizard/intropage.ui @@ -0,0 +1,34 @@ + + + IntroPage + + + + 0 + 0 + 474 + 370 + + + + WizardPage + + + Welcome to the OpenMW Wizard + + + + + + This Wizard will help you install Morrowind and its add-ons for OpenMW to use. + + + true + + + + + + + + diff --git a/files/ui/wizard/methodselectionpage.ui b/files/ui/wizard/methodselectionpage.ui new file mode 100644 index 0000000000..e77349f5cc --- /dev/null +++ b/files/ui/wizard/methodselectionpage.ui @@ -0,0 +1,156 @@ + + + MethodSelectionPage + + + + 0 + 0 + 398 + 298 + + + + WizardPage + + + Select Installation Method + + + Select how OpenMW should get the required Morrowind installation files. + + + + + + font-weight:bold; + + + Install to a new location + + + true + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 20 + 20 + + + + + + + + + 0 + 0 + + + + <html><head/><body><p><img src=":/icons/tango/48x48/system-installer.png"/></p></body></html> + + + Qt::RichText + + + false + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + newLocationRadioButton + + + + + + + Install Morrowind to a new location for OpenMW to use. + + + true + + + + + + + + + font-weight:bold + + + Select an existing installation + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 20 + 20 + + + + + + + + + 0 + 0 + + + + <html><head/><body><p><img src=":/icons/tango/48x48/folder.png"/></p></body></html> + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Select an existing Morrowind installation for OpenMW to use. + + + true + + + + + + + + + + + + diff --git a/files/wizard/icons/tango/48x48/folder.png b/files/wizard/icons/tango/48x48/folder.png new file mode 100644 index 0000000000000000000000000000000000000000..e93d7cf8f8460c1576f40b6981d42181a1e1b38f GIT binary patch literal 1610 zcmV-Q2DSN#P)$Qp1=mSL zK~!ko?U~DqWJwjqe-ZbQkIIKN(@NvQh5aXpOL5^W8pPGW;L?rD?gl|-(F>P0xaw}) z%tG)F(4~zi!fZqvM22Bt#Dy>vgWXnERc7A2_r~+M=zDXrvZ^byYUoOoIuPW^xWwiA z;yWkeL|nMB8@sXpX;Z6z9zJ~d6Q$JqB9a0qr99voKPaVa<42{GSs7XZgb z=+U39i$J>iM?`*h_wL%jxO;fG4 ze)sy z2wSPJ9#Xe&-yZdPJ@)ta>G%6+tx-y$)Tu>}hSc&|2!UtMo_+7$y?eiY`t<4S(gZ>X z)>_N)@i9k7N5mK}rNU|`0UR70EJ3#*+orW%5^McD&jCVF6k}j^6$NVN9HY@_Is2;w zHmR^qpatIkt}4;`Ns=rHXsr>En5Jp(sv%T+@0rbJ93CDn0l!X#^(0;gze=JdvOepp zewJkb(HPUcDuK#5$8b2rT1yCl-~H;Go+S#O ze{<;b?2q4l@B_R49_PcSfBxkU-?=CUDr+r8Q81ZI2q7@7Z}HO~eTQK$yROYu17BV5 zENjotfA!(pTPZLY45+FK<07Ew zKBeE)2(mNq08xa>c(i76o?NR6l&u1z(TKD=V$|>8{qDf~ASk7%YKId2;z9zg0!2}9 ze0n3j%yzk|Ex5Ng54D9!S!DJ$zlc)2NwYin(66;NTo zpx432cRPCIjEK81NHQh{MLuh97RlNEjXI61woCSctsXCWKs? zrrH?8yw;>|=Tt79jd|G?sBG?)&E<4iJG?h9HxcL_92|7-+_{rvS%&KLQAvuife_@& z&FCdM*B_e@S(Mhfay|z}Kmo*c0y(fZola8`NsKX6Zop6!#%)esx%7kWLrk2oFvh!j zRsn-%==?kao%wvOVvI?aWq933rwL(qhq2{x($prH*^3F#0Su6}-!1f74v11J{JTgf zLr$;X-7R{7>B(G7ZNuk(|LsT3{fk=tFw-jFn`VtMAN=*jTR#q(?30iG$<8hMmoGp0 z!~50bVAfm~y9VnQ*Gi2IfIXlO^cuO6MkTe2^L%^K1!|ydTG0?WLja(=0zPdBBs(Gy zn$^ZPfK5XnoVnOLuZC(8MX&D_*BLww*wCmT+}MrX*#DpX7iXTUrIOxbGynhq07*qo IM6N<$f?B=}0ssI2 literal 0 HcmV?d00001 diff --git a/files/wizard/icons/tango/48x48/system-installer.png b/files/wizard/icons/tango/48x48/system-installer.png new file mode 100644 index 0000000000000000000000000000000000000000..dbd08f7e2f5d40f313206326b379ccc467c139c1 GIT binary patch literal 2663 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 + + icons/tango/index.theme + icons/tango/48x48/folder.png + icons/tango/48x48/system-installer.png + + From 8162f8370b21d4b666693a3260fdd5cbc1bb6b03 Mon Sep 17 00:00:00 2001 From: pvdk Date: Sun, 8 Dec 2013 21:35:57 +0100 Subject: [PATCH 003/303] Added all the pages to the wizard --- apps/wizard/CMakeLists.txt | 32 +++++++++++-- apps/wizard/componentselectionpage.cpp | 7 +++ apps/wizard/componentselectionpage.hpp | 21 ++++++++ apps/wizard/conclusionpage.cpp | 7 +++ apps/wizard/conclusionpage.hpp | 21 ++++++++ apps/wizard/existinginstallationpage.cpp | 7 +++ apps/wizard/existinginstallationpage.hpp | 21 ++++++++ apps/wizard/importpage.cpp | 7 +++ apps/wizard/importpage.hpp | 21 ++++++++ apps/wizard/installationpage.cpp | 7 +++ apps/wizard/installationpage.hpp | 21 ++++++++ apps/wizard/installationtargetpage.cpp | 7 +++ apps/wizard/installationtargetpage.hpp | 21 ++++++++ apps/wizard/intropage.cpp | 7 +++ apps/wizard/intropage.hpp | 21 ++++++++ apps/wizard/main.cpp | 11 ++--- apps/wizard/mainwizard.cpp | 36 ++++++++++++++ apps/wizard/mainwizard.hpp | 33 +++++++++++++ apps/wizard/methodselectionpage.cpp | 7 +++ apps/wizard/methodselectionpage.hpp | 21 ++++++++ apps/wizard/wizard.cpp | 1 - apps/wizard/wizard.hpp | 1 - files/ui/wizard/configurationpage.ui | 58 ----------------------- files/ui/wizard/installationtargetpage.ui | 10 ---- 24 files changed, 324 insertions(+), 82 deletions(-) create mode 100644 apps/wizard/componentselectionpage.cpp create mode 100644 apps/wizard/componentselectionpage.hpp create mode 100644 apps/wizard/conclusionpage.cpp create mode 100644 apps/wizard/conclusionpage.hpp create mode 100644 apps/wizard/existinginstallationpage.cpp create mode 100644 apps/wizard/existinginstallationpage.hpp create mode 100644 apps/wizard/importpage.cpp create mode 100644 apps/wizard/importpage.hpp create mode 100644 apps/wizard/installationpage.cpp create mode 100644 apps/wizard/installationpage.hpp create mode 100644 apps/wizard/installationtargetpage.cpp create mode 100644 apps/wizard/installationtargetpage.hpp create mode 100644 apps/wizard/intropage.cpp create mode 100644 apps/wizard/intropage.hpp create mode 100644 apps/wizard/mainwizard.cpp create mode 100644 apps/wizard/mainwizard.hpp create mode 100644 apps/wizard/methodselectionpage.cpp create mode 100644 apps/wizard/methodselectionpage.hpp delete mode 100644 apps/wizard/wizard.cpp delete mode 100644 apps/wizard/wizard.hpp delete mode 100644 files/ui/wizard/configurationpage.ui diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index d6423daad4..3ff41c206b 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -1,14 +1,29 @@ set(WIZARD + componentselectionpage.cpp + conclusionpage.cpp + existinginstallationpage.cpp + importpage.cpp + installationpage.cpp + installationtargetpage.cpp + intropage.cpp main.cpp - wizard.cpp - + mainwizard.cpp + methodselectionpage.cpp ) # if(NOT WIN32) # LIST(APPEND WIZARD unshieldthread.cpp) # endif(NOT WIN32) set(WIZARD_HEADER - wizard.hpp + componentselectionpage.hpp + conclusionpage.hpp + existinginstallationpage.hpp + importpage.hpp + installationpage.hpp + installationtargetpage.hpp + intropage.hpp + mainwizard.hpp + methodselectionpage.hpp ) # if(NOT WIN32) # LIST(APPEND WIZARD_HEADER unshieldthread.hpp) @@ -17,7 +32,15 @@ set(WIZARD_HEADER # Headers that must be pre-processed set(WIZARD_HEADER_MOC - wizard.hpp + componentselectionpage.hpp + conclusionpage.hpp + existinginstallationpage.hpp + importpage.hpp + installationpage.hpp + installationtargetpage.hpp + intropage.hpp + mainwizard.hpp + methodselectionpage.hpp ) # if(NOT WIN32) @@ -28,7 +51,6 @@ set(WIZARD_HEADER_MOC set(WIZARD_UI ${CMAKE_SOURCE_DIR}/files/ui/wizard/componentselectionpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/conclusionpage.ui - ${CMAKE_SOURCE_DIR}/files/ui/wizard/configurationpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/existinginstallationpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/importpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationpage.ui diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp new file mode 100644 index 0000000000..dbfa9412ea --- /dev/null +++ b/apps/wizard/componentselectionpage.cpp @@ -0,0 +1,7 @@ +#include "componentselectionpage.hpp" + +Wizard::ComponentSelectionPage::ComponentSelectionPage(QWidget *parent) : + QWizardPage(parent) +{ + setupUi(this); +} diff --git a/apps/wizard/componentselectionpage.hpp b/apps/wizard/componentselectionpage.hpp new file mode 100644 index 0000000000..cc36ee4855 --- /dev/null +++ b/apps/wizard/componentselectionpage.hpp @@ -0,0 +1,21 @@ +#ifndef COMPONENTSELECTIONPAGE_HPP +#define COMPONENTSELECTIONPAGE_HPP + +#include + +#include "ui_componentselectionpage.h" + +namespace Wizard +{ + + class ComponentSelectionPage : public QWizardPage, private Ui::ComponentSelectionPage + { + Q_OBJECT + public: + ComponentSelectionPage(QWidget *parent = 0); + + }; + +} + +#endif // COMPONENTSELECTIONPAGE_HPP diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp new file mode 100644 index 0000000000..2913e431df --- /dev/null +++ b/apps/wizard/conclusionpage.cpp @@ -0,0 +1,7 @@ +#include "conclusionpage.hpp" + +Wizard::ConclusionPage::ConclusionPage(QWidget *parent) : + QWizardPage(parent) +{ + setupUi(this); +} diff --git a/apps/wizard/conclusionpage.hpp b/apps/wizard/conclusionpage.hpp new file mode 100644 index 0000000000..37c477b446 --- /dev/null +++ b/apps/wizard/conclusionpage.hpp @@ -0,0 +1,21 @@ +#ifndef CONCLUSIONPAGE_HPP +#define CONCLUSIONPAGE_HPP + +#include + +#include "ui_conclusionpage.h" + +namespace Wizard +{ + + class ConclusionPage : public QWizardPage, private Ui::ConclusionPage + { + Q_OBJECT + public: + ConclusionPage(QWidget *parent = 0); + + }; + +} + +#endif // CONCLUSIONPAGE_HPP diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp new file mode 100644 index 0000000000..ad540de73f --- /dev/null +++ b/apps/wizard/existinginstallationpage.cpp @@ -0,0 +1,7 @@ +#include "existinginstallationpage.hpp" + +Wizard::ExistingInstallationPage::ExistingInstallationPage(QWidget *parent) : + QWizardPage(parent) +{ + setupUi(this); +} diff --git a/apps/wizard/existinginstallationpage.hpp b/apps/wizard/existinginstallationpage.hpp new file mode 100644 index 0000000000..5048a14d43 --- /dev/null +++ b/apps/wizard/existinginstallationpage.hpp @@ -0,0 +1,21 @@ +#ifndef EXISTINGINSTALLATIONPAGE_HPP +#define EXISTINGINSTALLATIONPAGE_HPP + +#include + +#include "ui_existinginstallationpage.h" + +namespace Wizard +{ + + class ExistingInstallationPage : public QWizardPage, private Ui::ExistingInstallationPage + { + Q_OBJECT + public: + ExistingInstallationPage(QWidget *parent = 0); + + }; + +} + +#endif // EXISTINGINSTALLATIONPAGE_HPP diff --git a/apps/wizard/importpage.cpp b/apps/wizard/importpage.cpp new file mode 100644 index 0000000000..2ac92efd0e --- /dev/null +++ b/apps/wizard/importpage.cpp @@ -0,0 +1,7 @@ +#include "importpage.hpp" + +Wizard::ImportPage::ImportPage(QWidget *parent) : + QWizardPage(parent) +{ + setupUi(this); +} diff --git a/apps/wizard/importpage.hpp b/apps/wizard/importpage.hpp new file mode 100644 index 0000000000..e3917c837d --- /dev/null +++ b/apps/wizard/importpage.hpp @@ -0,0 +1,21 @@ +#ifndef IMPORTPAGE_HPP +#define IMPORTPAGE_HPP + +#include + +#include "ui_importpage.h" + +namespace Wizard +{ + + class ImportPage : public QWizardPage, private Ui::ImportPage + { + Q_OBJECT + public: + ImportPage(QWidget *parent = 0); + + }; + +} + +#endif // IMPORTPAGE_HPP diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp new file mode 100644 index 0000000000..20737ebfba --- /dev/null +++ b/apps/wizard/installationpage.cpp @@ -0,0 +1,7 @@ +#include "installationpage.hpp" + +Wizard::InstallationPage::InstallationPage(QWidget *parent) : + QWizardPage(parent) +{ + setupUi(this); +} diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp new file mode 100644 index 0000000000..3c4470047d --- /dev/null +++ b/apps/wizard/installationpage.hpp @@ -0,0 +1,21 @@ +#ifndef INSTALLATIONPAGE_HPP +#define INSTALLATIONPAGE_HPP + +#include + +#include "ui_installationpage.h" + +namespace Wizard +{ + + class InstallationPage : public QWizardPage, private Ui::InstallationPage + { + Q_OBJECT + public: + InstallationPage(QWidget *parent = 0); + + }; + +} + +#endif // INSTALLATIONPAGE_HPP diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp new file mode 100644 index 0000000000..7632b23312 --- /dev/null +++ b/apps/wizard/installationtargetpage.cpp @@ -0,0 +1,7 @@ +#include "installationtargetpage.hpp" + +Wizard::InstallationTargetPage::InstallationTargetPage(QWidget *parent) : + QWizardPage(parent) +{ + setupUi(this); +} diff --git a/apps/wizard/installationtargetpage.hpp b/apps/wizard/installationtargetpage.hpp new file mode 100644 index 0000000000..266d1adc7d --- /dev/null +++ b/apps/wizard/installationtargetpage.hpp @@ -0,0 +1,21 @@ +#ifndef INSTALLATIONTARGETPAGE_HPP +#define INSTALLATIONTARGETPAGE_HPP + +#include + +#include "ui_installationtargetpage.h" + +namespace Wizard +{ + + class InstallationTargetPage : public QWizardPage, private Ui::InstallationTargetPage + { + Q_OBJECT + public: + InstallationTargetPage(QWidget *parent = 0); + + }; + +} + +#endif // INSTALLATIONTARGETPAGE_HPP diff --git a/apps/wizard/intropage.cpp b/apps/wizard/intropage.cpp new file mode 100644 index 0000000000..7b390f0595 --- /dev/null +++ b/apps/wizard/intropage.cpp @@ -0,0 +1,7 @@ +#include "intropage.hpp" + +Wizard::IntroPage::IntroPage(QWidget *parent) : + QWizardPage(parent) +{ + setupUi(this); +} diff --git a/apps/wizard/intropage.hpp b/apps/wizard/intropage.hpp new file mode 100644 index 0000000000..b247efd89f --- /dev/null +++ b/apps/wizard/intropage.hpp @@ -0,0 +1,21 @@ +#ifndef INTROPAGE_HPP +#define INTROPAGE_HPP + +#include + +#include "ui_intropage.h" + +namespace Wizard +{ + + class IntroPage : public QWizardPage, private Ui::IntroPage + { + Q_OBJECT + public: + IntroPage(QWidget *parent = 0); + + }; + +} + +#endif // INTROPAGE_HPP diff --git a/apps/wizard/main.cpp b/apps/wizard/main.cpp index ed3f3a6eae..e6a94118af 100644 --- a/apps/wizard/main.cpp +++ b/apps/wizard/main.cpp @@ -3,6 +3,8 @@ #include #include +#include "mainwizard.hpp" + #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 @@ -39,13 +41,8 @@ int main(int argc, char *argv[]) // Support non-latin characters QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8")); -// Launcher::MainDialog mainWin; - -// if (mainWin.setup()) { -// mainWin.show(); -// } else { -// return 0; -// } + Wizard::MainWizard wizard; + wizard.show(); return app.exec(); } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp new file mode 100644 index 0000000000..438d9e5763 --- /dev/null +++ b/apps/wizard/mainwizard.cpp @@ -0,0 +1,36 @@ +#include "mainwizard.hpp" +#include "intropage.hpp" +#include "methodselectionpage.hpp" +#include "existinginstallationpage.hpp" +#include "installationtargetpage.hpp" +#include "componentselectionpage.hpp" +#include "installationpage.hpp" +#include "importpage.hpp" +#include "conclusionpage.hpp" + +Wizard::MainWizard::MainWizard(QWidget *parent) : + QWizard(parent) +{ + +#ifndef Q_OS_MAC + setWizardStyle(QWizard::ModernStyle); +#else + setWizardStyle(QWizard::ClassicStyle); +#endif + + setWindowTitle(tr("OpenMW Wizard")); + setupPages(); +} + +void Wizard::MainWizard::setupPages() +{ + setPage(Page_Intro, new IntroPage); + setPage(Page_MethodSelection, new MethodSelectionPage); + setPage(Page_ExistingInstallation, new ExistingInstallationPage); + setPage(Page_InstallationTarget, new InstallationTargetPage); + setPage(Page_ComponentSelection, new ComponentSelectionPage); + setPage(Page_Installation, new InstallationPage); + setPage(Page_Import, new ImportPage); + setPage(Page_Conclusion, new ConclusionPage); + setStartId(Page_Intro); +} diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp new file mode 100644 index 0000000000..e5cdc866bd --- /dev/null +++ b/apps/wizard/mainwizard.hpp @@ -0,0 +1,33 @@ +#ifndef MAINWIZARD_HPP +#define MAINWIZARD_HPP + +#include + +namespace Wizard +{ + + class MainWizard : public QWizard + { + Q_OBJECT + + public: + enum { + Page_Intro, + Page_MethodSelection, + Page_ExistingInstallation, + Page_InstallationTarget, + Page_ComponentSelection, + Page_Installation, + Page_Import, + Page_Conclusion + }; + + MainWizard(QWidget *parent = 0); + + private: + void setupPages(); + }; + +} + +#endif // MAINWIZARD_HPP diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp new file mode 100644 index 0000000000..bd9997367b --- /dev/null +++ b/apps/wizard/methodselectionpage.cpp @@ -0,0 +1,7 @@ +#include "methodselectionpage.hpp" + +Wizard::MethodSelectionPage::MethodSelectionPage(QWidget *parent) : + QWizardPage(parent) +{ + setupUi(this); +} diff --git a/apps/wizard/methodselectionpage.hpp b/apps/wizard/methodselectionpage.hpp new file mode 100644 index 0000000000..cc32a825ee --- /dev/null +++ b/apps/wizard/methodselectionpage.hpp @@ -0,0 +1,21 @@ +#ifndef METHODSELECTIONPAGE_HPP +#define METHODSELECTIONPAGE_HPP + +#include + +#include "ui_methodselectionpage.h" + +namespace Wizard +{ + + class MethodSelectionPage : public QWizardPage, private Ui::MethodSelectionPage + { + Q_OBJECT + public: + MethodSelectionPage(QWidget *parent = 0); + + }; + +} + +#endif // METHODSELECTIONPAGE_HPP diff --git a/apps/wizard/wizard.cpp b/apps/wizard/wizard.cpp deleted file mode 100644 index 8d1c8b69c3..0000000000 --- a/apps/wizard/wizard.cpp +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/wizard/wizard.hpp b/apps/wizard/wizard.hpp deleted file mode 100644 index 8d1c8b69c3..0000000000 --- a/apps/wizard/wizard.hpp +++ /dev/null @@ -1 +0,0 @@ - diff --git a/files/ui/wizard/configurationpage.ui b/files/ui/wizard/configurationpage.ui deleted file mode 100644 index 8340b2ebf9..0000000000 --- a/files/ui/wizard/configurationpage.ui +++ /dev/null @@ -1,58 +0,0 @@ - - - WizardPage - - - - 0 - 0 - 440 - 300 - - - - WizardPage - - - - - - <html><head/><body><p>OpenMW needs to import settings from the Morrowind configuration file in order to function properly. Select the steps you want to be taken and click Next when you are ready to continue.</p><p><span style=" font-weight:600;">Note:</span> it is possible to import settings later, by re-running this Wizard.</p><p/></body></html> - - - true - - - - - - - Import settings from Morrowind.ini - - - - - - - Import selected add-ons (creates a new Cotent List in the launcher) - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - diff --git a/files/ui/wizard/installationtargetpage.ui b/files/ui/wizard/installationtargetpage.ui index 6bdac55927..c87214d8d9 100644 --- a/files/ui/wizard/installationtargetpage.ui +++ b/files/ui/wizard/installationtargetpage.ui @@ -53,16 +53,6 @@ - - - - Click Next to continue. If you would like to select a different location, click Browse. - - - true - - - From 0e1d3237fe280a07a228eda14008bc116e36463a Mon Sep 17 00:00:00 2001 From: pvdk Date: Sun, 8 Dec 2013 22:58:29 +0100 Subject: [PATCH 004/303] Added some logic to the page switching --- apps/wizard/componentselectionpage.cpp | 7 +++++++ apps/wizard/componentselectionpage.hpp | 2 ++ apps/wizard/conclusionpage.cpp | 5 +++++ apps/wizard/conclusionpage.hpp | 2 ++ apps/wizard/existinginstallationpage.cpp | 7 +++++++ apps/wizard/existinginstallationpage.hpp | 2 ++ apps/wizard/importpage.cpp | 7 +++++++ apps/wizard/importpage.hpp | 2 ++ apps/wizard/installationpage.cpp | 7 +++++++ apps/wizard/installationpage.hpp | 2 ++ apps/wizard/installationtargetpage.cpp | 7 +++++++ apps/wizard/installationtargetpage.hpp | 2 ++ apps/wizard/intropage.cpp | 7 +++++++ apps/wizard/intropage.hpp | 2 ++ apps/wizard/methodselectionpage.cpp | 11 +++++++++++ apps/wizard/methodselectionpage.hpp | 2 ++ 16 files changed, 74 insertions(+) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index dbfa9412ea..0293bdc054 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -1,7 +1,14 @@ #include "componentselectionpage.hpp" +#include "mainwizard.hpp" + Wizard::ComponentSelectionPage::ComponentSelectionPage(QWidget *parent) : QWizardPage(parent) { setupUi(this); } + +int Wizard::ComponentSelectionPage::nextId() const +{ + return MainWizard::Page_Installation; +} diff --git a/apps/wizard/componentselectionpage.hpp b/apps/wizard/componentselectionpage.hpp index cc36ee4855..277845774c 100644 --- a/apps/wizard/componentselectionpage.hpp +++ b/apps/wizard/componentselectionpage.hpp @@ -14,6 +14,8 @@ namespace Wizard public: ComponentSelectionPage(QWidget *parent = 0); + int nextId() const; + }; } diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index 2913e431df..567d325b3e 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -5,3 +5,8 @@ Wizard::ConclusionPage::ConclusionPage(QWidget *parent) : { setupUi(this); } + +int Wizard::ConclusionPage::nextId() const +{ + return -1; +} diff --git a/apps/wizard/conclusionpage.hpp b/apps/wizard/conclusionpage.hpp index 37c477b446..e00fc0fe3b 100644 --- a/apps/wizard/conclusionpage.hpp +++ b/apps/wizard/conclusionpage.hpp @@ -14,6 +14,8 @@ namespace Wizard public: ConclusionPage(QWidget *parent = 0); + int nextId() const; + }; } diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index ad540de73f..d6dea0af92 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -1,7 +1,14 @@ #include "existinginstallationpage.hpp" +#include "mainwizard.hpp" + Wizard::ExistingInstallationPage::ExistingInstallationPage(QWidget *parent) : QWizardPage(parent) { setupUi(this); } + +int Wizard::ExistingInstallationPage::nextId() const +{ + return MainWizard::Page_ComponentSelection; +} diff --git a/apps/wizard/existinginstallationpage.hpp b/apps/wizard/existinginstallationpage.hpp index 5048a14d43..823bc2140b 100644 --- a/apps/wizard/existinginstallationpage.hpp +++ b/apps/wizard/existinginstallationpage.hpp @@ -14,6 +14,8 @@ namespace Wizard public: ExistingInstallationPage(QWidget *parent = 0); + int nextId() const; + }; } diff --git a/apps/wizard/importpage.cpp b/apps/wizard/importpage.cpp index 2ac92efd0e..29db6919dd 100644 --- a/apps/wizard/importpage.cpp +++ b/apps/wizard/importpage.cpp @@ -1,7 +1,14 @@ #include "importpage.hpp" +#include "mainwizard.hpp" + Wizard::ImportPage::ImportPage(QWidget *parent) : QWizardPage(parent) { setupUi(this); } + +int Wizard::ImportPage::nextId() const +{ + return MainWizard::Page_Conclusion; +} diff --git a/apps/wizard/importpage.hpp b/apps/wizard/importpage.hpp index e3917c837d..f1cc11e03c 100644 --- a/apps/wizard/importpage.hpp +++ b/apps/wizard/importpage.hpp @@ -14,6 +14,8 @@ namespace Wizard public: ImportPage(QWidget *parent = 0); + int nextId() const; + }; } diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 20737ebfba..092cf62e0e 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -1,7 +1,14 @@ #include "installationpage.hpp" +#include "mainwizard.hpp" + Wizard::InstallationPage::InstallationPage(QWidget *parent) : QWizardPage(parent) { setupUi(this); } + +int Wizard::InstallationPage::nextId() const +{ + return MainWizard::Page_Import; +} diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index 3c4470047d..125fcc2e1e 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -14,6 +14,8 @@ namespace Wizard public: InstallationPage(QWidget *parent = 0); + int nextId() const; + }; } diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index 7632b23312..fa178f9136 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -1,7 +1,14 @@ #include "installationtargetpage.hpp" +#include "mainwizard.hpp" + Wizard::InstallationTargetPage::InstallationTargetPage(QWidget *parent) : QWizardPage(parent) { setupUi(this); } + +int Wizard::InstallationTargetPage::nextId() const +{ + return MainWizard::Page_ComponentSelection; +} diff --git a/apps/wizard/installationtargetpage.hpp b/apps/wizard/installationtargetpage.hpp index 266d1adc7d..4e399b707d 100644 --- a/apps/wizard/installationtargetpage.hpp +++ b/apps/wizard/installationtargetpage.hpp @@ -14,6 +14,8 @@ namespace Wizard public: InstallationTargetPage(QWidget *parent = 0); + int nextId() const; + }; } diff --git a/apps/wizard/intropage.cpp b/apps/wizard/intropage.cpp index 7b390f0595..72c769ae6a 100644 --- a/apps/wizard/intropage.cpp +++ b/apps/wizard/intropage.cpp @@ -1,7 +1,14 @@ #include "intropage.hpp" +#include "mainwizard.hpp" + Wizard::IntroPage::IntroPage(QWidget *parent) : QWizardPage(parent) { setupUi(this); } + +int Wizard::IntroPage::nextId() const +{ + return MainWizard::Page_MethodSelection; +} diff --git a/apps/wizard/intropage.hpp b/apps/wizard/intropage.hpp index b247efd89f..603d283fa4 100644 --- a/apps/wizard/intropage.hpp +++ b/apps/wizard/intropage.hpp @@ -14,6 +14,8 @@ namespace Wizard public: IntroPage(QWidget *parent = 0); + int nextId() const; + }; } diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp index bd9997367b..59125f2518 100644 --- a/apps/wizard/methodselectionpage.cpp +++ b/apps/wizard/methodselectionpage.cpp @@ -1,7 +1,18 @@ #include "methodselectionpage.hpp" +#include "mainwizard.hpp" + Wizard::MethodSelectionPage::MethodSelectionPage(QWidget *parent) : QWizardPage(parent) { setupUi(this); } + +int Wizard::MethodSelectionPage::nextId() const +{ + if (newLocationRadioButton->isChecked()) { + return MainWizard::Page_InstallationTarget; + } else { + return MainWizard::Page_ExistingInstallation; + } +} diff --git a/apps/wizard/methodselectionpage.hpp b/apps/wizard/methodselectionpage.hpp index cc32a825ee..1555ea157d 100644 --- a/apps/wizard/methodselectionpage.hpp +++ b/apps/wizard/methodselectionpage.hpp @@ -14,6 +14,8 @@ namespace Wizard public: MethodSelectionPage(QWidget *parent = 0); + int nextId() const; + }; } From d7f96041400c8398da4fd061b3218671e598d81a Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 13 Dec 2013 13:38:49 +0100 Subject: [PATCH 005/303] WIP: Working on the installation selection and added the ui for the language page --- apps/wizard/componentselectionpage.cpp | 107 +++++++++++++++++++- apps/wizard/componentselectionpage.hpp | 12 ++- apps/wizard/conclusionpage.cpp | 21 +++- apps/wizard/conclusionpage.hpp | 9 +- apps/wizard/existinginstallationpage.cpp | 97 +++++++++++++++++- apps/wizard/existinginstallationpage.hpp | 15 ++- apps/wizard/importpage.cpp | 5 +- apps/wizard/importpage.hpp | 6 +- apps/wizard/installationpage.cpp | 13 ++- apps/wizard/installationpage.hpp | 9 +- apps/wizard/installationtargetpage.cpp | 33 +++++- apps/wizard/installationtargetpage.hpp | 12 ++- apps/wizard/intropage.cpp | 6 +- apps/wizard/intropage.hpp | 5 +- apps/wizard/mainwizard.cpp | 69 +++++++++++-- apps/wizard/mainwizard.hpp | 13 +++ apps/wizard/methodselectionpage.cpp | 11 +- apps/wizard/methodselectionpage.hpp | 6 +- files/ui/wizard/conclusionpage.ui | 8 +- files/ui/wizard/existinginstallationpage.ui | 9 +- files/ui/wizard/languageselectionpage.ui | 72 +++++++++++++ files/wizard/wizard.qrc | 7 +- 22 files changed, 499 insertions(+), 46 deletions(-) create mode 100644 files/ui/wizard/languageselectionpage.ui diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index 0293bdc054..1ec1380b13 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -1,14 +1,115 @@ #include "componentselectionpage.hpp" +#include +#include + #include "mainwizard.hpp" -Wizard::ComponentSelectionPage::ComponentSelectionPage(QWidget *parent) : - QWizardPage(parent) +Wizard::ComponentSelectionPage::ComponentSelectionPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) { setupUi(this); + + setCommitPage(true); + setButtonText(QWizard::CommitButton, tr("&Install")); + + connect(componentsList, SIGNAL(itemChanged(QListWidgetItem*)), + this, SLOT(updateButton(QListWidgetItem*))); +} + +void Wizard::ComponentSelectionPage::updateButton(QListWidgetItem *item) +{ + if (field("installation.new").toBool() == true) + return; // Morrowind is always checked here + + bool unchecked = true; + + for (int i =0; i < componentsList->count(); ++i) { + QListWidgetItem *item = componentsList->item(i); + + qDebug() << "item is " << item->text(); + + if (!item) + continue; + + if (item->checkState() == Qt::Checked) { + unchecked = false; + } + } + + if (unchecked) { + setCommitPage(false); + setButtonText(QWizard::NextButton, tr("&Skip")); + } else { + setCommitPage(true); + } +} + +void Wizard::ComponentSelectionPage::initializePage() +{ + componentsList->clear(); + + QString path = field("installation.path").toString(); + + QListWidgetItem *morrowindItem = new QListWidgetItem(QString("Morrowind")); + QListWidgetItem *tribunalItem = new QListWidgetItem(QString("Tribunal")); + QListWidgetItem *bloodmoonItem = new QListWidgetItem(QString("Bloodmoon")); + + if (field("installation.new").toBool() == true) + { + morrowindItem->setFlags(morrowindItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); + morrowindItem->setData(Qt::CheckStateRole, Qt::Checked); + componentsList->addItem(morrowindItem); + + tribunalItem->setFlags(tribunalItem->flags() | Qt::ItemIsUserCheckable); + tribunalItem->setData(Qt::CheckStateRole, Qt::Checked); + componentsList->addItem(tribunalItem); + + bloodmoonItem->setFlags(bloodmoonItem->flags() | Qt::ItemIsUserCheckable); + bloodmoonItem->setData(Qt::CheckStateRole, Qt::Checked); + componentsList->addItem(bloodmoonItem); + } else { + + if (mWizard->mInstallations[path]->hasMorrowind == true) { + morrowindItem->setText(tr("Morrowind\t\t(installed)")); + morrowindItem->setFlags(morrowindItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); + morrowindItem->setData(Qt::CheckStateRole, Qt::Unchecked); + } else { + morrowindItem->setText(tr("Morrowind")); + morrowindItem->setData(Qt::CheckStateRole, Qt::Checked); + } + + componentsList->addItem(morrowindItem); + + if (mWizard->mInstallations[path]->hasTribunal == true) { + tribunalItem->setText(tr("Tribunal\t\t(installed)")); + tribunalItem->setFlags(tribunalItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); + tribunalItem->setData(Qt::CheckStateRole, Qt::Unchecked); + } else { + tribunalItem->setText(tr("Tribunal")); + tribunalItem->setData(Qt::CheckStateRole, Qt::Checked); + } + + componentsList->addItem(tribunalItem); + + if (mWizard->mInstallations[path]->hasBloodmoon == true) { + bloodmoonItem->setText(tr("Bloodmoon\t\t(installed)")); + bloodmoonItem->setFlags(bloodmoonItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); + bloodmoonItem->setData(Qt::CheckStateRole, Qt::Unchecked); + } else { + bloodmoonItem->setText(tr("Bloodmoon")); + bloodmoonItem->setData(Qt::CheckStateRole, Qt::Checked); + } + + componentsList->addItem(bloodmoonItem); + } } int Wizard::ComponentSelectionPage::nextId() const { - return MainWizard::Page_Installation; + if (isCommitPage()) + return MainWizard::Page_Installation; + + return MainWizard::Page_Import; } diff --git a/apps/wizard/componentselectionpage.hpp b/apps/wizard/componentselectionpage.hpp index 277845774c..8b4c186d09 100644 --- a/apps/wizard/componentselectionpage.hpp +++ b/apps/wizard/componentselectionpage.hpp @@ -7,15 +7,25 @@ namespace Wizard { + class MainWizard; class ComponentSelectionPage : public QWizardPage, private Ui::ComponentSelectionPage { Q_OBJECT public: - ComponentSelectionPage(QWidget *parent = 0); + ComponentSelectionPage(MainWizard *wizard); int nextId() const; + private slots: + void updateButton(QListWidgetItem *item); + + private: + MainWizard *mWizard; + + protected: + void initializePage(); + }; } diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index 567d325b3e..55b03b9129 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -1,11 +1,28 @@ #include "conclusionpage.hpp" -Wizard::ConclusionPage::ConclusionPage(QWidget *parent) : - QWizardPage(parent) +#include + +#include "mainwizard.hpp" + +Wizard::ConclusionPage::ConclusionPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) { setupUi(this); } +void Wizard::ConclusionPage::initializePage() +{ + if (field("installation.new").toBool() == true) + { + textLabel->setText(tr("The OpenMW Wizard successfully installed Morrowind on your computer.\n\n") + + tr("Click Finish to close the Wizard.")); + } else { + textLabel->setText(tr("The OpenMW Wizard successfully modified your existing Morrowind installation.\n\n") + + tr("Click Finish to close the Wizard.")); + } +} + int Wizard::ConclusionPage::nextId() const { return -1; diff --git a/apps/wizard/conclusionpage.hpp b/apps/wizard/conclusionpage.hpp index e00fc0fe3b..e050684e15 100644 --- a/apps/wizard/conclusionpage.hpp +++ b/apps/wizard/conclusionpage.hpp @@ -7,15 +7,22 @@ namespace Wizard { + class MainWizard; class ConclusionPage : public QWizardPage, private Ui::ConclusionPage { Q_OBJECT public: - ConclusionPage(QWidget *parent = 0); + ConclusionPage(MainWizard *wizard); int nextId() const; + private: + MainWizard *mWizard; + + protected: + void initializePage(); + }; } diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index d6dea0af92..f9eb7c283c 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -1,14 +1,105 @@ #include "existinginstallationpage.hpp" +#include +#include + #include "mainwizard.hpp" -Wizard::ExistingInstallationPage::ExistingInstallationPage(QWidget *parent) : - QWizardPage(parent) +Wizard::ExistingInstallationPage::ExistingInstallationPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) { setupUi(this); + + connect(detectedList, SIGNAL(currentTextChanged(QString)), + this, SLOT(textChanged(QString))); + + connect(detectedList,SIGNAL(itemSelectionChanged()), + this, SIGNAL(completeChanged())); +} + +void Wizard::ExistingInstallationPage::on_browseButton_clicked() +{ + QString selectedFile = QFileDialog::getOpenFileName( + this, + tr("Select master file"), + QDir::currentPath(), + QString(tr("Morrowind master file (*.esm)")), + NULL, + QFileDialog::DontResolveSymlinks); + + QFileInfo info(selectedFile); + if (!info.exists()) + return; + + QDir dir(info.absolutePath()); + if (!dir.cdUp()) + return; // Cannot move out of the Data Files directory + + QString path = QDir::toNativeSeparators(dir.absolutePath()); + QList items = detectedList->findItems(path, Qt::MatchExactly); + + if (items.isEmpty()) + { + // Path is not yet in the list, add it + QListWidgetItem *item = new QListWidgetItem(path); + detectedList->addItem(item); + detectedList->setCurrentItem(item); // Select it too + } else { + detectedList->setCurrentItem(items.first()); + } + +} + +void Wizard::ExistingInstallationPage::textChanged(const QString &text) +{ + // Set the installation path manually, as registerField doesn't work + if (!text.isEmpty()) + wizard()->setField("installation.path", text); +} + +void Wizard::ExistingInstallationPage::initializePage() +{ + + QStringList paths = mWizard->mInstallations.keys(); + + if (paths.isEmpty()) + return; + + detectedList->clear(); + + foreach (const QString &path, paths) { + QListWidgetItem *item = new QListWidgetItem(path); + detectedList->addItem(item); + } + +} + +bool Wizard::ExistingInstallationPage::isComplete() const +{ + if (detectedList->selectionModel()->hasSelection()) { + return true; + } else { + return false; + } } int Wizard::ExistingInstallationPage::nextId() const { - return MainWizard::Page_ComponentSelection; + QString path = field("installation.path").toString(); + + if (path.isEmpty()) + return MainWizard::Page_ComponentSelection; + + if (!mWizard->mInstallations.contains(path)) + return MainWizard::Page_ComponentSelection; + + if (mWizard->mInstallations[path]->hasMorrowind == true && + mWizard->mInstallations[path]->hasTribunal == true && + mWizard->mInstallations[path]->hasBloodmoon == true) + { + return MainWizard::Page_Import; + } else { + return MainWizard::Page_ComponentSelection; + } } diff --git a/apps/wizard/existinginstallationpage.hpp b/apps/wizard/existinginstallationpage.hpp index 823bc2140b..88d6913bc2 100644 --- a/apps/wizard/existinginstallationpage.hpp +++ b/apps/wizard/existinginstallationpage.hpp @@ -7,14 +7,27 @@ namespace Wizard { + class MainWizard; class ExistingInstallationPage : public QWizardPage, private Ui::ExistingInstallationPage { Q_OBJECT public: - ExistingInstallationPage(QWidget *parent = 0); + ExistingInstallationPage(MainWizard *wizard); int nextId() const; + virtual bool isComplete() const; + + private slots: + void on_browseButton_clicked(); + void textChanged(const QString &text); + + + private: + MainWizard *mWizard; + + protected: + void initializePage(); }; diff --git a/apps/wizard/importpage.cpp b/apps/wizard/importpage.cpp index 29db6919dd..059683cf0a 100644 --- a/apps/wizard/importpage.cpp +++ b/apps/wizard/importpage.cpp @@ -2,8 +2,9 @@ #include "mainwizard.hpp" -Wizard::ImportPage::ImportPage(QWidget *parent) : - QWizardPage(parent) +Wizard::ImportPage::ImportPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) { setupUi(this); } diff --git a/apps/wizard/importpage.hpp b/apps/wizard/importpage.hpp index f1cc11e03c..2eae08531c 100644 --- a/apps/wizard/importpage.hpp +++ b/apps/wizard/importpage.hpp @@ -7,15 +7,19 @@ namespace Wizard { + class MainWizard; class ImportPage : public QWizardPage, private Ui::ImportPage { Q_OBJECT public: - ImportPage(QWidget *parent = 0); + ImportPage(MainWizard *wizard); int nextId() const; + private: + MainWizard *mWizard; + }; } diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 092cf62e0e..de0720ecbd 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -1,13 +1,22 @@ #include "installationpage.hpp" +#include + #include "mainwizard.hpp" -Wizard::InstallationPage::InstallationPage(QWidget *parent) : - QWizardPage(parent) +Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) { setupUi(this); } +void Wizard::InstallationPage::initializePage() +{ + qDebug() << "installing to: " << field("installation.path").toString(); + logTextEdit->setText(QString("Installing to %1").arg(field("installation.path").toString())); +} + int Wizard::InstallationPage::nextId() const { return MainWizard::Page_Import; diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index 125fcc2e1e..cb206e2251 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -7,15 +7,22 @@ namespace Wizard { + class MainWizard; class InstallationPage : public QWizardPage, private Ui::InstallationPage { Q_OBJECT public: - InstallationPage(QWidget *parent = 0); + InstallationPage(MainWizard *wizard); int nextId() const; + private: + MainWizard *mWizard; + + protected: + void initializePage(); + }; } diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index fa178f9136..43941f0dca 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -1,11 +1,40 @@ #include "installationtargetpage.hpp" +#include +#include + #include "mainwizard.hpp" -Wizard::InstallationTargetPage::InstallationTargetPage(QWidget *parent) : - QWizardPage(parent) +Wizard::InstallationTargetPage::InstallationTargetPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) { setupUi(this); + + registerField("installation.path*", targetLineEdit); +} + +void Wizard::InstallationTargetPage::initializePage() +{ + +} + +void Wizard::InstallationTargetPage::on_browseButton_clicked() +{ + QString selectedPath = QFileDialog::getExistingDirectory( + this, + tr("Select where to install Morrowind"), + QDir::currentPath(), + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + qDebug() << selectedPath; + QFileInfo info(selectedPath); + if (!info.exists()) + return; + + if (info.isWritable()) + targetLineEdit->setText(info.absoluteFilePath()); + } int Wizard::InstallationTargetPage::nextId() const diff --git a/apps/wizard/installationtargetpage.hpp b/apps/wizard/installationtargetpage.hpp index 4e399b707d..ccaac5969d 100644 --- a/apps/wizard/installationtargetpage.hpp +++ b/apps/wizard/installationtargetpage.hpp @@ -7,15 +7,25 @@ namespace Wizard { + class MainWizard; class InstallationTargetPage : public QWizardPage, private Ui::InstallationTargetPage { Q_OBJECT public: - InstallationTargetPage(QWidget *parent = 0); + InstallationTargetPage(MainWizard *wizard); int nextId() const; + private slots: + void on_browseButton_clicked(); + + private: + MainWizard *mWizard; + + protected: + void initializePage(); + }; } diff --git a/apps/wizard/intropage.cpp b/apps/wizard/intropage.cpp index 72c769ae6a..93510cd210 100644 --- a/apps/wizard/intropage.cpp +++ b/apps/wizard/intropage.cpp @@ -2,8 +2,10 @@ #include "mainwizard.hpp" -Wizard::IntroPage::IntroPage(QWidget *parent) : - QWizardPage(parent) +Wizard::IntroPage::IntroPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) + { setupUi(this); } diff --git a/apps/wizard/intropage.hpp b/apps/wizard/intropage.hpp index 603d283fa4..78bac3155e 100644 --- a/apps/wizard/intropage.hpp +++ b/apps/wizard/intropage.hpp @@ -7,15 +7,18 @@ namespace Wizard { + class MainWizard; class IntroPage : public QWizardPage, private Ui::IntroPage { Q_OBJECT public: - IntroPage(QWidget *parent = 0); + IntroPage(MainWizard *wizard); int nextId() const; + private: + MainWizard *mWizard; }; } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 438d9e5763..a741546fdc 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -1,4 +1,8 @@ #include "mainwizard.hpp" + +#include +#include + #include "intropage.hpp" #include "methodselectionpage.hpp" #include "existinginstallationpage.hpp" @@ -19,18 +23,67 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : #endif setWindowTitle(tr("OpenMW Wizard")); + setupInstallations(); setupPages(); + + QDir dir("/home/pvdk/data"); + QFileInfo info(dir.absoluteFilePath("../Morrowind.ini")); + + qDebug() << "exists? " << info.exists(); +} + +void Wizard::MainWizard::setupInstallations() +{ + // TODO: detect existing installations + QStringList paths; + paths << QString("/home/pvdk/.wine/drive_c/Program Files/Bethesda Softworks/Morrowind"); + paths << QString("/home/pvdk/openmw/Data Files"); + paths << QString("/usr/games/morrowind"); + + foreach (const QString &path, paths) + { + Installation* install = new Installation(); + + install->hasMorrowind = (findFiles(QString("Morrowind"), path)); + install->hasTribunal = true; + install->hasBloodmoon = false; + + mInstallations.insert(QDir::toNativeSeparators(path), install); + } + } void Wizard::MainWizard::setupPages() { - setPage(Page_Intro, new IntroPage); - setPage(Page_MethodSelection, new MethodSelectionPage); - setPage(Page_ExistingInstallation, new ExistingInstallationPage); - setPage(Page_InstallationTarget, new InstallationTargetPage); - setPage(Page_ComponentSelection, new ComponentSelectionPage); - setPage(Page_Installation, new InstallationPage); - setPage(Page_Import, new ImportPage); - setPage(Page_Conclusion, new ConclusionPage); + setPage(Page_Intro, new IntroPage(this)); + setPage(Page_MethodSelection, new MethodSelectionPage(this)); + setPage(Page_ExistingInstallation, new ExistingInstallationPage(this)); + setPage(Page_InstallationTarget, new InstallationTargetPage(this)); + setPage(Page_ComponentSelection, new ComponentSelectionPage(this)); + setPage(Page_Installation, new InstallationPage(this)); + setPage(Page_Import, new ImportPage(this)); + setPage(Page_Conclusion, new ConclusionPage(this)); setStartId(Page_Intro); } + +bool Wizard::MainWizard::findFiles(const QString &name, const QString &path) +{ + QDir dir(path); + + if (!dir.exists()) + return false; + + if (!dir.cd(QString("Data Files"))) + return false; + + qDebug() << "name: " << name + QString(".esm") << dir.absolutePath(); + + // TODO: add MIME handling to make sure the files are real + if (dir.exists(name + QString(".esm")) && dir.exists(name + QString(".bsa"))) + { + return true; + } else { + return false; + } + +} diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index e5cdc866bd..15706e8e35 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -2,6 +2,7 @@ #define MAINWIZARD_HPP #include +#include namespace Wizard { @@ -11,6 +12,12 @@ namespace Wizard Q_OBJECT public: + struct Installation { + bool hasMorrowind; + bool hasTribunal; + bool hasBloodmoon; + }; + enum { Page_Intro, Page_MethodSelection, @@ -24,8 +31,14 @@ namespace Wizard MainWizard(QWidget *parent = 0); + static bool findFiles(const QString &name, const QString &path); + QMap mInstallations; + private: + void setupInstallations(); void setupPages(); + + }; } diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp index 59125f2518..1efffc13d8 100644 --- a/apps/wizard/methodselectionpage.cpp +++ b/apps/wizard/methodselectionpage.cpp @@ -1,18 +1,23 @@ #include "methodselectionpage.hpp" - +#include #include "mainwizard.hpp" -Wizard::MethodSelectionPage::MethodSelectionPage(QWidget *parent) : - QWizardPage(parent) +Wizard::MethodSelectionPage::MethodSelectionPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) { setupUi(this); + + registerField("installation.new", newLocationRadioButton); } int Wizard::MethodSelectionPage::nextId() const { if (newLocationRadioButton->isChecked()) { + //wizard()->setField("installation.new", true); return MainWizard::Page_InstallationTarget; } else { + //wizard()->setField("installation.new", false); return MainWizard::Page_ExistingInstallation; } } diff --git a/apps/wizard/methodselectionpage.hpp b/apps/wizard/methodselectionpage.hpp index 1555ea157d..de34ff555d 100644 --- a/apps/wizard/methodselectionpage.hpp +++ b/apps/wizard/methodselectionpage.hpp @@ -7,15 +7,19 @@ namespace Wizard { + class MainWizard; class MethodSelectionPage : public QWizardPage, private Ui::MethodSelectionPage { Q_OBJECT public: - MethodSelectionPage(QWidget *parent = 0); + MethodSelectionPage(MainWizard *wizard); int nextId() const; + private: + MainWizard *mWizard; + }; } diff --git a/files/ui/wizard/conclusionpage.ui b/files/ui/wizard/conclusionpage.ui index bedad3e2aa..a27f667899 100644 --- a/files/ui/wizard/conclusionpage.ui +++ b/files/ui/wizard/conclusionpage.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 300 + 398 + 298 @@ -20,9 +20,7 @@ - The OpenMW Wizard successfully installed Morrowind on your computer. - -Click Finish to close the Wizard. + Placeholder true diff --git a/files/ui/wizard/existinginstallationpage.ui b/files/ui/wizard/existinginstallationpage.ui index 579ddbe672..3ca8316361 100644 --- a/files/ui/wizard/existinginstallationpage.ui +++ b/files/ui/wizard/existinginstallationpage.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 300 + 396 + 296 @@ -23,7 +23,7 @@ - Detected Installations: + Detected installations: @@ -33,6 +33,9 @@ No existing installations detected + + NoItemFlags + diff --git a/files/ui/wizard/languageselectionpage.ui b/files/ui/wizard/languageselectionpage.ui new file mode 100644 index 0000000000..134a9de86b --- /dev/null +++ b/files/ui/wizard/languageselectionpage.ui @@ -0,0 +1,72 @@ + + + WizardPage + + + + 0 + 0 + 398 + 298 + + + + WizardPage + + + + + + + + + 0 + 0 + + + + <html><head/><body><p><img src=":/icons/tango/48x48/preferences-desktop-locale.png"/></p></body></html> + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + What is the language of the Morrowind installation? + + + true + + + + + + + + + + + + Qt::Vertical + + + + 20 + 230 + + + + + + + + + + + diff --git a/files/wizard/wizard.qrc b/files/wizard/wizard.qrc index 86b3795c44..b3bb722c9f 100644 --- a/files/wizard/wizard.qrc +++ b/files/wizard/wizard.qrc @@ -1,7 +1,8 @@ - - + + + icons/tango/48x48/preferences-desktop-locale.png icons/tango/index.theme icons/tango/48x48/folder.png icons/tango/48x48/system-installer.png - + From c82f0ec35fb6464fd1485e61f0633cb51c8a8660 Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 13 Dec 2013 15:30:49 +0100 Subject: [PATCH 006/303] Implemented the language selection page --- apps/wizard/CMakeLists.txt | 4 ++ apps/wizard/languageselectionpage.cpp | 18 +++++ apps/wizard/languageselectionpage.hpp | 25 +++++++ apps/wizard/mainwizard.cpp | 2 + apps/wizard/mainwizard.hpp | 1 + apps/wizard/methodselectionpage.cpp | 8 +-- files/ui/wizard/languageselectionpage.ui | 88 ++++++++++++++++++++++-- 7 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 apps/wizard/languageselectionpage.cpp create mode 100644 apps/wizard/languageselectionpage.hpp diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index 3ff41c206b..a62327d85b 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -6,6 +6,7 @@ set(WIZARD installationpage.cpp installationtargetpage.cpp intropage.cpp + languageselectionpage.cpp main.cpp mainwizard.cpp methodselectionpage.cpp @@ -22,6 +23,7 @@ set(WIZARD_HEADER installationpage.hpp installationtargetpage.hpp intropage.hpp + languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp ) @@ -39,6 +41,7 @@ set(WIZARD_HEADER_MOC installationpage.hpp installationtargetpage.hpp intropage.hpp + languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp ) @@ -56,6 +59,7 @@ set(WIZARD_UI ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationtargetpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/intropage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/languageselectionpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/methodselectionpage.ui ) diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp new file mode 100644 index 0000000000..dc64f1ae1b --- /dev/null +++ b/apps/wizard/languageselectionpage.cpp @@ -0,0 +1,18 @@ +#include "languageselectionpage.hpp" + +#include "mainwizard.hpp" + +Wizard::LanguageSelectionPage::LanguageSelectionPage(MainWizard *wizard) : + QWizardPage(wizard), + mWizard(wizard) +{ + setupUi(this); +} + +int Wizard::LanguageSelectionPage::nextId() const +{ + if (field("installation.new").toBool() == true) + return MainWizard::Page_InstallationTarget; + + return MainWizard::Page_ExistingInstallation; +} diff --git a/apps/wizard/languageselectionpage.hpp b/apps/wizard/languageselectionpage.hpp new file mode 100644 index 0000000000..aa7a81ff17 --- /dev/null +++ b/apps/wizard/languageselectionpage.hpp @@ -0,0 +1,25 @@ +#ifndef LANGUAGESELECTIONPAGE_HPP +#define LANGUAGESELECTIONPAGE_HPP + +#include + +#include "ui_languageselectionpage.h" + +namespace Wizard +{ + class MainWizard; + + class LanguageSelectionPage : public QWizardPage, private Ui::LanguageSelectionPage + { + Q_OBJECT + public: + LanguageSelectionPage(MainWizard *wizard); + + int nextId() const; + + private: + MainWizard *mWizard; + }; +} + +#endif // LANGUAGESELECTIONPAGE_HPP diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index a741546fdc..77f17499ef 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -5,6 +5,7 @@ #include "intropage.hpp" #include "methodselectionpage.hpp" +#include "languageselectionpage.hpp" #include "existinginstallationpage.hpp" #include "installationtargetpage.hpp" #include "componentselectionpage.hpp" @@ -57,6 +58,7 @@ void Wizard::MainWizard::setupPages() { setPage(Page_Intro, new IntroPage(this)); setPage(Page_MethodSelection, new MethodSelectionPage(this)); + setPage(Page_LanguageSelection, new LanguageSelectionPage(this)); setPage(Page_ExistingInstallation, new ExistingInstallationPage(this)); setPage(Page_InstallationTarget, new InstallationTargetPage(this)); setPage(Page_ComponentSelection, new ComponentSelectionPage(this)); diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index 15706e8e35..c6b541d35b 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -21,6 +21,7 @@ namespace Wizard enum { Page_Intro, Page_MethodSelection, + Page_LanguageSelection, Page_ExistingInstallation, Page_InstallationTarget, Page_ComponentSelection, diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp index 1efffc13d8..4185fa2bcb 100644 --- a/apps/wizard/methodselectionpage.cpp +++ b/apps/wizard/methodselectionpage.cpp @@ -13,11 +13,5 @@ Wizard::MethodSelectionPage::MethodSelectionPage(MainWizard *wizard) : int Wizard::MethodSelectionPage::nextId() const { - if (newLocationRadioButton->isChecked()) { - //wizard()->setField("installation.new", true); - return MainWizard::Page_InstallationTarget; - } else { - //wizard()->setField("installation.new", false); - return MainWizard::Page_ExistingInstallation; - } + return MainWizard::Page_LanguageSelection; } diff --git a/files/ui/wizard/languageselectionpage.ui b/files/ui/wizard/languageselectionpage.ui index 134a9de86b..a76e3004b3 100644 --- a/files/ui/wizard/languageselectionpage.ui +++ b/files/ui/wizard/languageselectionpage.ui @@ -1,7 +1,7 @@ - WizardPage - + LanguageSelectionPage + 0 @@ -13,11 +13,17 @@ WizardPage + + Select Morrowind Language + + + What is the language of the Morrowind installation? + - + 0 @@ -38,7 +44,7 @@ - What is the language of the Morrowind installation? + Select the language of the Morrowind installation. true @@ -48,7 +54,79 @@ - + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 250 + 0 + + + + + English + + + + + French + + + + + Russian + + + + + Polish + + + + + Italian + + + + + Spanish + + + + + German + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + From 7132b9b0c590574e4d603c605a7307ba23c1151c Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 13 Dec 2013 23:48:55 +0100 Subject: [PATCH 007/303] Implement default installation path support for all platforms --- apps/wizard/componentselectionpage.cpp | 6 +++--- apps/wizard/installationtargetpage.cpp | 28 +++++++++++++++++++++++++- apps/wizard/languageselectionpage.cpp | 2 ++ apps/wizard/mainwizard.cpp | 5 ----- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index 1ec1380b13..046f340765 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -52,9 +52,9 @@ void Wizard::ComponentSelectionPage::initializePage() QString path = field("installation.path").toString(); - QListWidgetItem *morrowindItem = new QListWidgetItem(QString("Morrowind")); - QListWidgetItem *tribunalItem = new QListWidgetItem(QString("Tribunal")); - QListWidgetItem *bloodmoonItem = new QListWidgetItem(QString("Bloodmoon")); + QListWidgetItem *morrowindItem = new QListWidgetItem(QLatin1String("Morrowind")); + QListWidgetItem *tribunalItem = new QListWidgetItem(QLatin1String("Tribunal")); + QListWidgetItem *bloodmoonItem = new QListWidgetItem(QLatin1String("Bloodmoon")); if (field("installation.new").toBool() == true) { diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index 43941f0dca..0952abdcb5 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "mainwizard.hpp" @@ -11,12 +12,37 @@ Wizard::InstallationTargetPage::InstallationTargetPage(MainWizard *wizard) : { setupUi(this); - registerField("installation.path*", targetLineEdit); + registerField(QLatin1String("installation.path*"), targetLineEdit); } void Wizard::InstallationTargetPage::initializePage() { +#ifdef Q_OS_WIN + QString path = QDesktopServices::storageLocation(QDesktopServices::DocumentsLocation); +#endif +#ifdef Q_OS_MAC + QString path = QDesktopServices::storageLocation(QDesktopServices::DataLocation); +#endif + +#if defined(Q_OS_LINUX) || defined(Q_OS_UNIX) + QString path = QFile::decodeName(qgetenv("XDG_DATA_HOME")); + + if (path.isEmpty()) + path = QDir::homePath() + QLatin1String("/.local/share"); +#endif + + path.append(QLatin1String("/openmw/data")); + + if (!QFile::exists(path)) { + QDir dir; + dir.mkpath(path); + } + + QDir dir(path); + targetLineEdit->setText(QDir::toNativeSeparators(dir.absolutePath())); + + qDebug() << path; } void Wizard::InstallationTargetPage::on_browseButton_clicked() diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp index dc64f1ae1b..903ab175f4 100644 --- a/apps/wizard/languageselectionpage.cpp +++ b/apps/wizard/languageselectionpage.cpp @@ -7,6 +7,8 @@ Wizard::LanguageSelectionPage::LanguageSelectionPage(MainWizard *wizard) : mWizard(wizard) { setupUi(this); + + registerField(QLatin1String("installation.language"), languagesComboBox); } int Wizard::LanguageSelectionPage::nextId() const diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 77f17499ef..e6cb140d08 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -26,11 +26,6 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setWindowTitle(tr("OpenMW Wizard")); setupInstallations(); setupPages(); - - QDir dir("/home/pvdk/data"); - QFileInfo info(dir.absoluteFilePath("../Morrowind.ini")); - - qDebug() << "exists? " << info.exists(); } void Wizard::MainWizard::setupInstallations() From 77fe73799af9e3638528d79e2c72443c884fcde0 Mon Sep 17 00:00:00 2001 From: pvdk Date: Sun, 15 Dec 2013 13:12:48 +0100 Subject: [PATCH 008/303] Enabled language setting to be accessable from different pages --- apps/wizard/componentselectionpage.cpp | 2 -- apps/wizard/installationpage.cpp | 2 ++ apps/wizard/installationtargetpage.cpp | 2 ++ apps/wizard/mainwizard.cpp | 4 ++++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index 046f340765..b5618c6771 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -28,8 +28,6 @@ void Wizard::ComponentSelectionPage::updateButton(QListWidgetItem *item) for (int i =0; i < componentsList->count(); ++i) { QListWidgetItem *item = componentsList->item(i); - qDebug() << "item is " << item->text(); - if (!item) continue; diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index de0720ecbd..1053c32401 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -13,6 +13,8 @@ Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : void Wizard::InstallationPage::initializePage() { + QString path = field("installation.path").toString(); + qDebug() << "installing to: " << field("installation.path").toString(); logTextEdit->setText(QString("Installing to %1").arg(field("installation.path").toString())); } diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index 0952abdcb5..60acfd5f78 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -17,6 +17,8 @@ Wizard::InstallationTargetPage::InstallationTargetPage(MainWizard *wizard) : void Wizard::InstallationTargetPage::initializePage() { + qDebug() << mWizard->field("installation.language"); + #ifdef Q_OS_WIN QString path = QDesktopServices::storageLocation(QDesktopServices::DocumentsLocation); #endif diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index e6cb140d08..58ef2e0611 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -24,6 +24,10 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : #endif setWindowTitle(tr("OpenMW Wizard")); + + // Set the property for comboboxes to the text instead of index + setDefaultProperty("QComboBox", "currentText", "currentIndexChanged"); + setupInstallations(); setupPages(); } From 30710bad704cc9fa2ad7c4efd534c3cd7d3b5cd7 Mon Sep 17 00:00:00 2001 From: pvdk Date: Sun, 15 Dec 2013 15:13:49 +0100 Subject: [PATCH 009/303] Modified components widget so the checked items can be stored for all pages to access --- apps/wizard/CMakeLists.txt | 6 +++ apps/wizard/componentselectionpage.cpp | 12 +++++- apps/wizard/componentselectionpage.hpp | 1 + apps/wizard/installationpage.cpp | 6 ++- apps/wizard/mainwizard.cpp | 2 + apps/wizard/utils/componentlistwidget.cpp | 46 +++++++++++++++++++++++ apps/wizard/utils/componentlistwidget.hpp | 29 ++++++++++++++ files/ui/wizard/componentselectionpage.ui | 13 +++++-- 8 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 apps/wizard/utils/componentlistwidget.cpp create mode 100644 apps/wizard/utils/componentlistwidget.hpp diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index a62327d85b..741f790b5a 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -10,6 +10,8 @@ set(WIZARD main.cpp mainwizard.cpp methodselectionpage.cpp + + utils/componentlistwidget.cpp ) # if(NOT WIN32) # LIST(APPEND WIZARD unshieldthread.cpp) @@ -26,6 +28,8 @@ set(WIZARD_HEADER languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp + + utils/componentlistwidget.hpp ) # if(NOT WIN32) # LIST(APPEND WIZARD_HEADER unshieldthread.hpp) @@ -44,6 +48,8 @@ set(WIZARD_HEADER_MOC languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp + + utils/componentlistwidget.hpp ) # if(NOT WIN32) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index b5618c6771..50e6e8ef44 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -14,8 +14,16 @@ Wizard::ComponentSelectionPage::ComponentSelectionPage(MainWizard *wizard) : setCommitPage(true); setButtonText(QWizard::CommitButton, tr("&Install")); - connect(componentsList, SIGNAL(itemChanged(QListWidgetItem*)), - this, SLOT(updateButton(QListWidgetItem*))); + registerField("installation.components", componentsList); + + connect(componentsList, SIGNAL(itemChanged(QListWidgetItem *)), + this, SLOT(updateButton(QListWidgetItem *))); + +} + +void Wizard::ComponentSelectionPage::debugMe(QString &text) +{ + qDebug() << "Debug Me" << text; } void Wizard::ComponentSelectionPage::updateButton(QListWidgetItem *item) diff --git a/apps/wizard/componentselectionpage.hpp b/apps/wizard/componentselectionpage.hpp index 8b4c186d09..f5e9557821 100644 --- a/apps/wizard/componentselectionpage.hpp +++ b/apps/wizard/componentselectionpage.hpp @@ -19,6 +19,7 @@ namespace Wizard private slots: void updateButton(QListWidgetItem *item); + void debugMe(QString &text); private: MainWizard *mWizard; diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 1053c32401..632b9c6a62 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -14,9 +14,11 @@ Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : void Wizard::InstallationPage::initializePage() { QString path = field("installation.path").toString(); + QStringList components = field("installation.components").toStringList(); + + logTextEdit->append(QString("Installing to %1").arg(path)); + logTextEdit->append(QString("Installing %1.").arg(components.join(", "))); - qDebug() << "installing to: " << field("installation.path").toString(); - logTextEdit->setText(QString("Installing to %1").arg(field("installation.path").toString())); } int Wizard::InstallationPage::nextId() const diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 58ef2e0611..e752d04558 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -28,6 +28,8 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : // Set the property for comboboxes to the text instead of index setDefaultProperty("QComboBox", "currentText", "currentIndexChanged"); + setDefaultProperty("ComponentListWidget", "mCheckedItems", "checkedItemsChanged"); + setupInstallations(); setupPages(); } diff --git a/apps/wizard/utils/componentlistwidget.cpp b/apps/wizard/utils/componentlistwidget.cpp new file mode 100644 index 0000000000..c9a1c19400 --- /dev/null +++ b/apps/wizard/utils/componentlistwidget.cpp @@ -0,0 +1,46 @@ +#include "componentlistwidget.hpp" + +#include +#include + +ComponentListWidget::ComponentListWidget(QWidget *parent) : + QListWidget(parent) +{ + mCheckedItems = QStringList(); + + connect(this, SIGNAL(itemChanged(QListWidgetItem *)), + this, SLOT(updateCheckedItems(QListWidgetItem *))); +} + +void ComponentListWidget::addItem(QListWidgetItem *item) +{ + // The model does not emit a dataChanged signal when items are added + // So we need to update manually + QListWidget::insertItem(count(), item); + updateCheckedItems(item); + +} + +QStringList ComponentListWidget::checkedItems() +{ + mCheckedItems.removeDuplicates(); + return mCheckedItems; +} + +void ComponentListWidget::updateCheckedItems(QListWidgetItem *item) +{ + QString text = item->text(); + + if (item->checkState() == Qt::Checked) { + if (!mCheckedItems.contains(text)) + mCheckedItems.append(text); + } else { + if (mCheckedItems.contains(text)) + mCheckedItems.removeAll(text); + } + + mCheckedItems.removeDuplicates(); + + emit checkedItemsChanged(mCheckedItems); + +} diff --git a/apps/wizard/utils/componentlistwidget.hpp b/apps/wizard/utils/componentlistwidget.hpp new file mode 100644 index 0000000000..c9c782d457 --- /dev/null +++ b/apps/wizard/utils/componentlistwidget.hpp @@ -0,0 +1,29 @@ +#ifndef COMPONENTLISTWIDGET_HPP +#define COMPONENTLISTWIDGET_HPP + +#include + +class ComponentListWidget : public QListWidget +{ + Q_OBJECT + + Q_PROPERTY(QStringList mCheckedItems READ checkedItems) + +public: + ComponentListWidget(QWidget *parent = 0); + + QStringList mCheckedItems; + QStringList checkedItems(); + + void addItem(QListWidgetItem *item); + +signals: + void checkedItemsChanged(const QStringList &items); + +private slots: + void updateCheckedItems(QListWidgetItem *item); +}; + + + +#endif // COMPONENTLISTWIDGET_HPP diff --git a/files/ui/wizard/componentselectionpage.ui b/files/ui/wizard/componentselectionpage.ui index c3333752ad..026fcdff6d 100644 --- a/files/ui/wizard/componentselectionpage.ui +++ b/files/ui/wizard/componentselectionpage.ui @@ -6,8 +6,8 @@ 0 0 - 454 - 393 + 448 + 387 @@ -38,7 +38,7 @@ - + @@ -55,6 +55,13 @@ + + + ComponentListWidget + QListWidget +
apps/wizard/utils/componentlistwidget.hpp
+
+
From 8ea31e5050f5ed83009b68a41effb57903218ee3 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 24 Dec 2013 19:38:21 +0100 Subject: [PATCH 010/303] Working on a Morrowind.ini reader --- apps/wizard/CMakeLists.txt | 32 ++++----------- apps/wizard/inisettings.cpp | 68 ++++++++++++++++++++++++++++++++ apps/wizard/inisettings.hpp | 45 +++++++++++++++++++++ apps/wizard/installationpage.cpp | 64 ++++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+), 24 deletions(-) create mode 100644 apps/wizard/inisettings.cpp create mode 100644 apps/wizard/inisettings.hpp diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index 741f790b5a..7639cc8e17 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -3,6 +3,7 @@ set(WIZARD conclusionpage.cpp existinginstallationpage.cpp importpage.cpp + inisettings.cpp installationpage.cpp installationtargetpage.cpp intropage.cpp @@ -10,31 +11,27 @@ set(WIZARD main.cpp mainwizard.cpp methodselectionpage.cpp + unshieldthread.cpp utils/componentlistwidget.cpp ) -# if(NOT WIN32) -# LIST(APPEND WIZARD unshieldthread.cpp) -# endif(NOT WIN32) set(WIZARD_HEADER componentselectionpage.hpp conclusionpage.hpp existinginstallationpage.hpp importpage.hpp + inisettings.hpp installationpage.hpp installationtargetpage.hpp intropage.hpp languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp + unshieldthread.hpp utils/componentlistwidget.hpp ) -# if(NOT WIN32) -# LIST(APPEND WIZARD_HEADER unshieldthread.hpp) -# endif(NOT WIN32) - # Headers that must be pre-processed set(WIZARD_HEADER_MOC @@ -48,15 +45,11 @@ set(WIZARD_HEADER_MOC languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp + unshieldthread.hpp utils/componentlistwidget.hpp ) -# if(NOT WIN32) -# LIST(APPEND WIZARD_HEADER_MOC unshieldthread.hpp) -# endif(NOT WIN32) - - set(WIZARD_UI ${CMAKE_SOURCE_DIR}/files/ui/wizard/componentselectionpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/conclusionpage.ui @@ -86,10 +79,7 @@ QT4_WRAP_UI(UI_HDRS ${WIZARD_UI}) include(${QT_USE_FILE}) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) -if(NOT WIN32) - include_directories(${LIBUNSHIELD_INCLUDE}) -endif(NOT WIN32) +include_directories(${CMAKE_CURRENT_BINARY_DIR} ${LIBUNSHIELD_INCLUDE}) add_executable(openmw-wizard ${GUI_TYPE} @@ -101,16 +91,10 @@ add_executable(openmw-wizard ) target_link_libraries(openmw-wizard -# ${Boost_LIBRARIES} + ${Boost_LIBRARIES} ${QT_LIBRARIES} + ${LIBUNSHIELD_LIBRARY} ) -if(NOT WIN32) - target_link_libraries(openmw-wizard - ${LIBUNSHIELD_LIBRARY} - ) -endif(NOT WIN32) - - if(DPKG_PROGRAM) INSTALL(TARGETS openmw-wizard RUNTIME DESTINATION games COMPONENT openmw-wizard) diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp new file mode 100644 index 0000000000..8bdac5ad0f --- /dev/null +++ b/apps/wizard/inisettings.cpp @@ -0,0 +1,68 @@ +#include "inisettings.hpp" + + +#include +#include +#include +#include +#include + +Wizard::IniSettings::IniSettings() +{ +} + +Wizard::IniSettings::~IniSettings() +{ +} + +bool Wizard::IniSettings::readFile(QTextStream &stream) +{ + // Look for a square bracket, "'\\[" + // that has one or more "not nothing" in it, "([^]]+)" + // and is closed with a square bracket, "\\]" + QRegExp sectionRe("^\\[([^]]+)\\]"); + + // Find any character(s) that is/are not equal sign(s), "[^=]+" + // followed by an optional whitespace, an equal sign, and another optional whitespace, "\\s*=\\s*" + // and one or more periods, "(.+)" + QRegExp keyRe("^([^=]+)\\s*=\\s*(.+)$"); + + QString currentSection; + + while (!stream.atEnd()) + { + const QString &line = stream.readLine(); + + if (line.isEmpty() || line.startsWith(";")) + continue; + + if (sectionRe.exactMatch(line)) + { + currentSection = sectionRe.cap(1); + } + else if (keyRe.indexIn(line) != -1) + { + QString key = keyRe.cap(1).trimmed(); + QString value = keyRe.cap(2).trimmed(); + + // Append the section, but only if there is one + if (!currentSection.isEmpty()) + key = currentSection + QLatin1Char('/') + key; + + mSettings[key] = QVariant(value); + } + } + + return true; +} + +bool Wizard::IniSettings::writeFile(QTextStream &stream) +{ + qDebug() << "test! " << stream.readAll(); + + while (!stream.atEnd()) { + qDebug() << "test! " << stream.readLine(); + } + + return true; +} diff --git a/apps/wizard/inisettings.hpp b/apps/wizard/inisettings.hpp new file mode 100644 index 0000000000..66fe96ec1f --- /dev/null +++ b/apps/wizard/inisettings.hpp @@ -0,0 +1,45 @@ +#ifndef INISETTINGS_HPP +#define INISETTINGS_HPP + +#include +#include + +class QTextStream; + +namespace Wizard +{ + + typedef QHash SettingsMap; + + class IniSettings + { + public: + explicit IniSettings(); + ~IniSettings(); + + inline QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const + { + return mSettings.value(key, defaultValue); + } + + inline void setValue(const QString &key, const QVariant &value) + { + mSettings.insert(key, value); + } + + inline void remove(const QString &key) + { + mSettings.remove(key); + } + + bool readFile(QTextStream &stream); + bool writeFile(QTextStream &stream); + + private: + + SettingsMap mSettings; + }; + +} + +#endif // INISETTINGS_HPP diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 632b9c6a62..5d943dbfa2 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -1,8 +1,12 @@ #include "installationpage.hpp" #include +#include +#include +#include #include "mainwizard.hpp" +#include "inisettings.hpp" Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : QWizardPage(wizard), @@ -19,6 +23,66 @@ void Wizard::InstallationPage::initializePage() logTextEdit->append(QString("Installing to %1").arg(path)); logTextEdit->append(QString("Installing %1.").arg(components.join(", "))); + installProgressBar->setMinimum(0); + + // Set the progressbar maximum to a multiple of 100 + // That way installing all three components would yield 300% + // When one component is done the bar will be filled by 33% + + if (field("installation.new").toBool() == true) + { + installProgressBar->setMaximum((components.count() * 100)); + } + else + { + if (components.contains("Tribunal") && mWizard->mInstallations[path]->hasTribunal == false) + installProgressBar->setMaximum(100); + + if (components.contains("Bloodmoon") && mWizard->mInstallations[path]->hasBloodmoon == false) + installProgressBar->setMaximum(installProgressBar->maximum() + 100); + } + + installProgressBar->setValue(100); + + // Test settings + IniSettings iniSettings; + + QFile file("/home/pvdk/.wine/drive_c/Program Files/Bethesda Softworks/Morrowind/Morrowind.ini"); + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(QObject::tr("
Could not open %0 for reading

\ + Please make sure you have the right permissions \ + and try again.
").arg(file.fileName())); + msgBox.exec(); + } + + QTextStream stream(&file); + + QString language = field("installation.language").toString(); + + if (language == QLatin1String("English") || + language == QLatin1String("German") || + language == QLatin1String("French")) + { + stream.setCodec(QTextCodec::codecForName("windows-1252")); + } + else if (language == QLatin1String("Russian")) + { + stream.setCodec(QTextCodec::codecForName("windows-1251")); + } + else if (language == QLatin1String("Polish")) + { + stream.setCodec(QTextCodec::codecForName("windows-1250")); + } + + iniSettings.readFile(stream); + + qDebug() << iniSettings.value("Game Files/GameFile0"); + } int Wizard::InstallationPage::nextId() const From 445f96434e537613234f7641ebaf28e9e0708a6f Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 24 Dec 2013 23:09:31 +0100 Subject: [PATCH 011/303] Added Morrowind.ini detection logic --- apps/wizard/componentselectionpage.cpp | 5 -- apps/wizard/componentselectionpage.hpp | 1 - apps/wizard/existinginstallationpage.cpp | 64 ++++++++++++++++++++---- apps/wizard/existinginstallationpage.hpp | 2 +- apps/wizard/inisettings.cpp | 2 +- apps/wizard/installationpage.cpp | 37 ++++++++------ apps/wizard/installationpage.hpp | 3 ++ apps/wizard/languageselectionpage.cpp | 16 +++++- apps/wizard/languageselectionpage.hpp | 3 ++ apps/wizard/mainwizard.cpp | 45 +++++++++++------ apps/wizard/mainwizard.hpp | 6 ++- files/ui/wizard/languageselectionpage.ui | 45 ++--------------- 12 files changed, 137 insertions(+), 92 deletions(-) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index 50e6e8ef44..7d934fe849 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -21,11 +21,6 @@ Wizard::ComponentSelectionPage::ComponentSelectionPage(MainWizard *wizard) : } -void Wizard::ComponentSelectionPage::debugMe(QString &text) -{ - qDebug() << "Debug Me" << text; -} - void Wizard::ComponentSelectionPage::updateButton(QListWidgetItem *item) { if (field("installation.new").toBool() == true) diff --git a/apps/wizard/componentselectionpage.hpp b/apps/wizard/componentselectionpage.hpp index f5e9557821..8b4c186d09 100644 --- a/apps/wizard/componentselectionpage.hpp +++ b/apps/wizard/componentselectionpage.hpp @@ -19,7 +19,6 @@ namespace Wizard private slots: void updateButton(QListWidgetItem *item); - void debugMe(QString &text); private: MainWizard *mWizard; diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index f9eb7c283c..aeaa5a1f6f 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -1,7 +1,10 @@ #include "existinginstallationpage.hpp" #include +#include #include +#include +#include #include "mainwizard.hpp" @@ -32,16 +35,14 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() if (!info.exists()) return; - QDir dir(info.absolutePath()); - if (!dir.cdUp()) - return; // Cannot move out of the Data Files directory - - QString path = QDir::toNativeSeparators(dir.absolutePath()); + QString path(QDir::toNativeSeparators(info.absolutePath())); QList items = detectedList->findItems(path, Qt::MatchExactly); if (items.isEmpty()) { // Path is not yet in the list, add it + mWizard->addInstallation(path); + QListWidgetItem *item = new QListWidgetItem(path); detectedList->addItem(item); detectedList->setCurrentItem(item); // Select it too @@ -55,13 +56,13 @@ void Wizard::ExistingInstallationPage::textChanged(const QString &text) { // Set the installation path manually, as registerField doesn't work if (!text.isEmpty()) - wizard()->setField("installation.path", text); + mWizard->setField("installation.path", text); } void Wizard::ExistingInstallationPage::initializePage() { - QStringList paths = mWizard->mInstallations.keys(); + QStringList paths(mWizard->mInstallations.keys()); if (paths.isEmpty()) return; @@ -75,6 +76,50 @@ void Wizard::ExistingInstallationPage::initializePage() } +bool Wizard::ExistingInstallationPage::validatePage() +{ + // See if Morrowind.ini is detected, if not, ask the user + // It can be missing entirely + // Or failed to be detected due to the target being a symlink + + QString path(field("installation.path").toString()); + QFile file(mWizard->mInstallations[path]->iniPath); + + if (!file.exists()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error detecting Morrowind configuration")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Cancel); + msgBox.setText(QObject::tr("
Could not find Morrowind.ini

\ + The Wizard needs to update settings in this file.

\ + Press \"Browse...\" to specify the location manually.
")); + + QAbstractButton *browseButton = + msgBox.addButton(QObject::tr("B&rowse..."), QMessageBox::ActionRole); + + msgBox.exec(); + + QString iniFile; + if (msgBox.clickedButton() == browseButton) { + iniFile = QFileDialog::getOpenFileName( + NULL, + QObject::tr("Select configuration file"), + QDir::currentPath(), + QString(tr("Morrowind configuration file (*.ini)"))); + } + + if (iniFile.isEmpty()) { + return false; // Cancel was clicked; + } + + // A proper Morrowind.ini was selected, set it + QFileInfo info(iniFile); + mWizard->mInstallations[path]->iniPath = info.absoluteFilePath(); + } + + return true; +} + bool Wizard::ExistingInstallationPage::isComplete() const { if (detectedList->selectionModel()->hasSelection()) { @@ -86,14 +131,11 @@ bool Wizard::ExistingInstallationPage::isComplete() const int Wizard::ExistingInstallationPage::nextId() const { - QString path = field("installation.path").toString(); + QString path(field("installation.path").toString()); if (path.isEmpty()) return MainWizard::Page_ComponentSelection; - if (!mWizard->mInstallations.contains(path)) - return MainWizard::Page_ComponentSelection; - if (mWizard->mInstallations[path]->hasMorrowind == true && mWizard->mInstallations[path]->hasTribunal == true && mWizard->mInstallations[path]->hasBloodmoon == true) diff --git a/apps/wizard/existinginstallationpage.hpp b/apps/wizard/existinginstallationpage.hpp index 88d6913bc2..d243bb8689 100644 --- a/apps/wizard/existinginstallationpage.hpp +++ b/apps/wizard/existinginstallationpage.hpp @@ -17,6 +17,7 @@ namespace Wizard int nextId() const; virtual bool isComplete() const; + virtual bool validatePage(); private slots: void on_browseButton_clicked(); @@ -28,7 +29,6 @@ namespace Wizard protected: void initializePage(); - }; } diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp index 8bdac5ad0f..2c099acecc 100644 --- a/apps/wizard/inisettings.cpp +++ b/apps/wizard/inisettings.cpp @@ -31,7 +31,7 @@ bool Wizard::IniSettings::readFile(QTextStream &stream) while (!stream.atEnd()) { - const QString &line = stream.readLine(); + QString line(stream.readLine()); if (line.isEmpty() || line.startsWith(";")) continue; diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 5d943dbfa2..e98d75ba4c 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -1,7 +1,9 @@ #include "installationpage.hpp" #include +#include #include +#include #include #include @@ -17,8 +19,8 @@ Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : void Wizard::InstallationPage::initializePage() { - QString path = field("installation.path").toString(); - QStringList components = field("installation.components").toStringList(); + QString path(field("installation.path").toString()); + QStringList components(field("installation.components").toStringList()); logTextEdit->append(QString("Installing to %1").arg(path)); logTextEdit->append(QString("Installing %1.").arg(components.join(", "))); @@ -44,45 +46,48 @@ void Wizard::InstallationPage::initializePage() installProgressBar->setValue(100); + if (field("installation.new").toBool() == false) + setupSettings(); +} + +void Wizard::InstallationPage::setupSettings() +{ // Test settings IniSettings iniSettings; - QFile file("/home/pvdk/.wine/drive_c/Program Files/Bethesda Softworks/Morrowind/Morrowind.ini"); + QString path(field("installation.path").toString()); + QFile file(mWizard->mInstallations[path]->iniPath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); + msgBox.setWindowTitle(tr("Error opening Morrowind configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText(QObject::tr("
Could not open %0 for reading

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); msgBox.exec(); + mWizard->close(); + return; } QTextStream stream(&file); - QString language = field("installation.language").toString(); + QString language(field("installation.language").toString()); - if (language == QLatin1String("English") || - language == QLatin1String("German") || - language == QLatin1String("French")) - { - stream.setCodec(QTextCodec::codecForName("windows-1252")); + if (language == QLatin1String("Polish")) { + stream.setCodec(QTextCodec::codecForName("windows-1250")); } - else if (language == QLatin1String("Russian")) - { + else if (language == QLatin1String("Russian")) { stream.setCodec(QTextCodec::codecForName("windows-1251")); } - else if (language == QLatin1String("Polish")) - { - stream.setCodec(QTextCodec::codecForName("windows-1250")); + else { + stream.setCodec(QTextCodec::codecForName("windows-1252")); } iniSettings.readFile(stream); qDebug() << iniSettings.value("Game Files/GameFile0"); - } int Wizard::InstallationPage::nextId() const diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index cb206e2251..b4fecc3ba8 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -20,9 +20,12 @@ namespace Wizard private: MainWizard *mWizard; + void setupSettings(); + protected: void initializePage(); + }; } diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp index 903ab175f4..102a4c39a5 100644 --- a/apps/wizard/languageselectionpage.cpp +++ b/apps/wizard/languageselectionpage.cpp @@ -8,7 +8,21 @@ Wizard::LanguageSelectionPage::LanguageSelectionPage(MainWizard *wizard) : { setupUi(this); - registerField(QLatin1String("installation.language"), languagesComboBox); + registerField(QLatin1String("installation.language"), languageComboBox); +} + +void Wizard::LanguageSelectionPage::initializePage() +{ + QStringList languages; + languages << "English" + << "French" + << "German" + << "Italian" + << "Polish" + << "Russian" + << "Spanish"; + + languageComboBox->addItems(languages); } int Wizard::LanguageSelectionPage::nextId() const diff --git a/apps/wizard/languageselectionpage.hpp b/apps/wizard/languageselectionpage.hpp index aa7a81ff17..3c17514f98 100644 --- a/apps/wizard/languageselectionpage.hpp +++ b/apps/wizard/languageselectionpage.hpp @@ -19,6 +19,9 @@ namespace Wizard private: MainWizard *mWizard; + + protected: + void initializePage(); }; } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index e752d04558..9b133cee19 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -38,23 +38,43 @@ void Wizard::MainWizard::setupInstallations() { // TODO: detect existing installations QStringList paths; - paths << QString("/home/pvdk/.wine/drive_c/Program Files/Bethesda Softworks/Morrowind"); + paths << QString("/home/pvdk/.wine/drive_c/Program Files/Bethesda Softworks/Morrowind/Data Files"); paths << QString("/home/pvdk/openmw/Data Files"); paths << QString("/usr/games/morrowind"); foreach (const QString &path, paths) { - Installation* install = new Installation(); - - install->hasMorrowind = (findFiles(QString("Morrowind"), path)); - install->hasTribunal = true; - install->hasBloodmoon = false; - - mInstallations.insert(QDir::toNativeSeparators(path), install); + addInstallation(path); } } +void Wizard::MainWizard::addInstallation(const QString &path) +{ + qDebug() << "add installation in: " << path; + Installation* install = new Installation(); + + install->hasMorrowind = findFiles(QLatin1String("Morrowind"), path); + install->hasTribunal = findFiles(QLatin1String("Tribunal"), path); + install->hasBloodmoon = findFiles(QLatin1String("Bloodmoon"), path); + + // Try to autodetect the Morrowind.ini location + QDir dir(path); + QFile file(dir.filePath("Morrowind.ini")); + + // Try the parent directory + // In normal Morrowind installations that's where Morrowind.ini is + if (!file.exists()) { + dir.cdUp(); + file.setFileName(dir.filePath(QLatin1String("Morrowind.ini"))); + } + + if (file.exists()) + install->iniPath = file.fileName(); + + mInstallations.insert(QDir::toNativeSeparators(path), install); +} + void Wizard::MainWizard::setupPages() { setPage(Page_Intro, new IntroPage(this)); @@ -76,16 +96,13 @@ bool Wizard::MainWizard::findFiles(const QString &name, const QString &path) if (!dir.exists()) return false; - if (!dir.cd(QString("Data Files"))) - return false; - - qDebug() << "name: " << name + QString(".esm") << dir.absolutePath(); - // TODO: add MIME handling to make sure the files are real - if (dir.exists(name + QString(".esm")) && dir.exists(name + QString(".bsa"))) + if (dir.exists(name + QLatin1String(".esm")) && dir.exists(name + QLatin1String(".bsa"))) { + qDebug() << name << " exists!"; return true; } else { + qDebug() << name << " doesn't exist!"; return false; } diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index c6b541d35b..c025c729e4 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -16,6 +16,8 @@ namespace Wizard bool hasMorrowind; bool hasTribunal; bool hasBloodmoon; + + QString iniPath; }; enum { @@ -32,7 +34,9 @@ namespace Wizard MainWizard(QWidget *parent = 0); - static bool findFiles(const QString &name, const QString &path); + bool findFiles(const QString &name, const QString &path); + void addInstallation(const QString &path); + QMap mInstallations; private: diff --git a/files/ui/wizard/languageselectionpage.ui b/files/ui/wizard/languageselectionpage.ui index a76e3004b3..fccd2aa424 100644 --- a/files/ui/wizard/languageselectionpage.ui +++ b/files/ui/wizard/languageselectionpage.ui @@ -6,8 +6,8 @@ 0 0 - 398 - 298 + 396 + 296 @@ -69,48 +69,13 @@ - + 250 0 - - - English - - - - - French - - - - - Russian - - - - - Polish - - - - - Italian - - - - - Spanish - - - - - German - - @@ -143,8 +108,6 @@ - - - + From 3a37761d76147bad3467e74c84911fe111ead020 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 24 Dec 2013 23:47:04 +0100 Subject: [PATCH 012/303] Some minor cleanups --- apps/wizard/existinginstallationpage.cpp | 19 ++++++++++--------- apps/wizard/mainwizard.cpp | 1 + apps/wizard/utils/componentlistwidget.hpp | 2 -- files/ui/wizard/existinginstallationpage.ui | 8 ++++---- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index aeaa5a1f6f..5c66ceb4f5 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -14,10 +14,10 @@ Wizard::ExistingInstallationPage::ExistingInstallationPage(MainWizard *wizard) : { setupUi(this); - connect(detectedList, SIGNAL(currentTextChanged(QString)), + connect(installationsList, SIGNAL(currentTextChanged(QString)), this, SLOT(textChanged(QString))); - connect(detectedList,SIGNAL(itemSelectionChanged()), + connect(installationsList,SIGNAL(itemSelectionChanged()), this, SIGNAL(completeChanged())); } @@ -36,7 +36,7 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() return; QString path(QDir::toNativeSeparators(info.absolutePath())); - QList items = detectedList->findItems(path, Qt::MatchExactly); + QList items = installationsList->findItems(path, Qt::MatchExactly); if (items.isEmpty()) { @@ -44,10 +44,10 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() mWizard->addInstallation(path); QListWidgetItem *item = new QListWidgetItem(path); - detectedList->addItem(item); - detectedList->setCurrentItem(item); // Select it too + installationsList->addItem(item); + installationsList->setCurrentItem(item); // Select it too } else { - detectedList->setCurrentItem(items.first()); + installationsList->setCurrentItem(items.first()); } } @@ -55,6 +55,7 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() void Wizard::ExistingInstallationPage::textChanged(const QString &text) { // Set the installation path manually, as registerField doesn't work + // Because it doesn't accept two widgets operating on a single field if (!text.isEmpty()) mWizard->setField("installation.path", text); } @@ -67,11 +68,11 @@ void Wizard::ExistingInstallationPage::initializePage() if (paths.isEmpty()) return; - detectedList->clear(); + installationsList->clear(); foreach (const QString &path, paths) { QListWidgetItem *item = new QListWidgetItem(path); - detectedList->addItem(item); + installationsList->addItem(item); } } @@ -122,7 +123,7 @@ bool Wizard::ExistingInstallationPage::validatePage() bool Wizard::ExistingInstallationPage::isComplete() const { - if (detectedList->selectionModel()->hasSelection()) { + if (installationsList->selectionModel()->hasSelection()) { return true; } else { return false; diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 9b133cee19..fde82e9984 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -29,6 +29,7 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setDefaultProperty("QComboBox", "currentText", "currentIndexChanged"); setDefaultProperty("ComponentListWidget", "mCheckedItems", "checkedItemsChanged"); + setDefaultProperty("InstallationListWidget", "mCurrentText", "currentTextChanged"); setupInstallations(); setupPages(); diff --git a/apps/wizard/utils/componentlistwidget.hpp b/apps/wizard/utils/componentlistwidget.hpp index c9c782d457..6869629d34 100644 --- a/apps/wizard/utils/componentlistwidget.hpp +++ b/apps/wizard/utils/componentlistwidget.hpp @@ -24,6 +24,4 @@ private slots: void updateCheckedItems(QListWidgetItem *item); }; - - #endif // COMPONENTLISTWIDGET_HPP diff --git a/files/ui/wizard/existinginstallationpage.ui b/files/ui/wizard/existinginstallationpage.ui index 3ca8316361..b887fb06cc 100644 --- a/files/ui/wizard/existinginstallationpage.ui +++ b/files/ui/wizard/existinginstallationpage.ui @@ -6,8 +6,8 @@ 0 0 - 396 - 296 + 394 + 294 @@ -21,14 +21,14 @@ - + Detected installations: - + No existing installations detected From 095ff4e17a54176716ee35b3f4c14082b5da6567 Mon Sep 17 00:00:00 2001 From: pvdk Date: Wed, 25 Dec 2013 00:50:25 +0100 Subject: [PATCH 013/303] Moved launcher settings stuff into components, so they can be reused in the wizard --- apps/launcher/CMakeLists.txt | 5 ----- apps/launcher/datafilespage.cpp | 12 +++++------- apps/launcher/datafilespage.hpp | 12 ++++++------ apps/launcher/maindialog.hpp | 9 +++++---- apps/launcher/settings/graphicssettings.hpp | 4 ++-- components/CMakeLists.txt | 13 +++++++++---- .../config}/gamesettings.cpp | 14 +++++++------- .../config}/gamesettings.hpp | 2 +- .../config}/launchersettings.cpp | 10 +++++----- .../config}/launchersettings.hpp | 2 +- .../config}/settingsbase.hpp | 2 +- 11 files changed, 42 insertions(+), 43 deletions(-) rename {apps/launcher/settings => components/config}/gamesettings.cpp (91%) rename {apps/launcher/settings => components/config}/gamesettings.hpp (98%) rename {apps/launcher/settings => components/config}/launchersettings.cpp (86%) rename {apps/launcher/settings => components/config}/launchersettings.hpp (96%) rename {apps/launcher/settings => components/config}/settingsbase.hpp (99%) diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index 18c555a249..2794744068 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -6,9 +6,7 @@ set(LAUNCHER playpage.cpp textslotmsgbox.cpp - settings/gamesettings.cpp settings/graphicssettings.cpp - settings/launchersettings.cpp utils/checkablemessagebox.cpp utils/profilescombobox.cpp @@ -28,10 +26,7 @@ set(LAUNCHER_HEADER playpage.hpp textslotmsgbox.hpp - settings/gamesettings.hpp settings/graphicssettings.hpp - settings/launchersettings.hpp - settings/settingsbase.hpp utils/checkablemessagebox.hpp utils/profilescombobox.hpp diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 362d7562c6..db59751027 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -9,18 +9,16 @@ #include #include - #include +#include + +#include +#include #include "utils/textinputdialog.hpp" #include "utils/profilescombobox.hpp" -#include "settings/gamesettings.hpp" -#include "settings/launchersettings.hpp" - -#include "components/contentselector/view/contentselector.hpp" - -Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, GameSettings &gameSettings, LauncherSettings &launcherSettings, QWidget *parent) +Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, Config::LauncherSettings &launcherSettings, QWidget *parent) : mCfgMgr(cfg) , mGameSettings(gameSettings) , mLauncherSettings(launcherSettings) diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index 37603a2106..642668cb74 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -14,12 +14,12 @@ class QMenu; namespace Files { struct ConfigurationManager; } namespace ContentSelectorView { class ContentSelector; } +namespace Config { class GameSettings; + class LauncherSettings; } namespace Launcher { class TextInputDialog; - class GameSettings; - class LauncherSettings; class ProfilesComboBox; class DataFilesPage : public QWidget @@ -30,8 +30,8 @@ namespace Launcher Ui::DataFilesPage ui; public: - explicit DataFilesPage (Files::ConfigurationManager &cfg, GameSettings &gameSettings, - LauncherSettings &launcherSettings, QWidget *parent = 0); + explicit DataFilesPage (Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, + Config::LauncherSettings &launcherSettings, QWidget *parent = 0); QAbstractItemModel* profilesModel() const; @@ -62,8 +62,8 @@ namespace Launcher Files::ConfigurationManager &mCfgMgr; - GameSettings &mGameSettings; - LauncherSettings &mLauncherSettings; + Config::GameSettings &mGameSettings; + Config::LauncherSettings &mLauncherSettings; QString mDataLocal; diff --git a/apps/launcher/maindialog.hpp b/apps/launcher/maindialog.hpp index 5b8e4908e7..a4fb4cd9a6 100644 --- a/apps/launcher/maindialog.hpp +++ b/apps/launcher/maindialog.hpp @@ -5,9 +5,10 @@ #ifndef Q_MOC_RUN #include #endif -#include "settings/gamesettings.hpp" +#include +#include + #include "settings/graphicssettings.hpp" -#include "settings/launchersettings.hpp" #include "ui_mainwindow.h" @@ -60,9 +61,9 @@ namespace Launcher Files::ConfigurationManager mCfgMgr; - GameSettings mGameSettings; + Config::GameSettings mGameSettings; GraphicsSettings mGraphicsSettings; - LauncherSettings mLauncherSettings; + Config::LauncherSettings mLauncherSettings; }; } diff --git a/apps/launcher/settings/graphicssettings.hpp b/apps/launcher/settings/graphicssettings.hpp index 6f7c135473..a52e0aa84c 100644 --- a/apps/launcher/settings/graphicssettings.hpp +++ b/apps/launcher/settings/graphicssettings.hpp @@ -1,11 +1,11 @@ #ifndef GRAPHICSSETTINGS_HPP #define GRAPHICSSETTINGS_HPP -#include "settingsbase.hpp" +#include namespace Launcher { - class GraphicsSettings : public SettingsBase > + class GraphicsSettings : public Config::SettingsBase > { public: GraphicsSettings(); diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 59fb084a8e..bfa25072ab 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -75,8 +75,8 @@ add_component_dir (loadinglistener ) add_component_dir (ogreinit - ogreinit ogreplugin - ) + ogreinit ogreplugin + ) set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui ) @@ -84,11 +84,16 @@ set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui find_package(Qt4 COMPONENTS QtCore QtGui) if(QT_QTGUI_LIBRARY AND QT_QTCORE_LIBRARY) - add_component_qt_dir (contentselector + add_component_qt_dir (contentselector model/modelitem model/esmfile model/naturalsort model/contentmodel view/combobox view/contentselector - ) + ) + add_component_qt_dir (config + gamesettings + launchersettings + settingsbase + ) include(${QT_USE_FILE}) QT4_WRAP_UI(ESM_UI_HDR ${ESM_UI}) diff --git a/apps/launcher/settings/gamesettings.cpp b/components/config/gamesettings.cpp similarity index 91% rename from apps/launcher/settings/gamesettings.cpp rename to components/config/gamesettings.cpp index 41113c35aa..62ae65df66 100644 --- a/apps/launcher/settings/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -27,16 +27,16 @@ namespace boost #endif /* (BOOST_VERSION <= 104600) */ -Launcher::GameSettings::GameSettings(Files::ConfigurationManager &cfg) +Config::GameSettings::GameSettings(Files::ConfigurationManager &cfg) : mCfgMgr(cfg) { } -Launcher::GameSettings::~GameSettings() +Config::GameSettings::~GameSettings() { } -void Launcher::GameSettings::validatePaths() +void Config::GameSettings::validatePaths() { if (mSettings.isEmpty() || !mDataDirs.isEmpty()) return; // Don't re-validate paths if they are already parsed @@ -82,14 +82,14 @@ void Launcher::GameSettings::validatePaths() } } -QStringList Launcher::GameSettings::values(const QString &key, const QStringList &defaultValues) +QStringList Config::GameSettings::values(const QString &key, const QStringList &defaultValues) { if (!mSettings.values(key).isEmpty()) return mSettings.values(key); return defaultValues; } -bool Launcher::GameSettings::readFile(QTextStream &stream) +bool Config::GameSettings::readFile(QTextStream &stream) { QMap cache; QRegExp keyRe("^([^=]+)\\s*=\\s*(.+)$"); @@ -131,7 +131,7 @@ bool Launcher::GameSettings::readFile(QTextStream &stream) return true; } -bool Launcher::GameSettings::writeFile(QTextStream &stream) +bool Config::GameSettings::writeFile(QTextStream &stream) { // Iterate in reverse order to preserve insertion order QMapIterator i(mSettings); @@ -170,7 +170,7 @@ bool Launcher::GameSettings::writeFile(QTextStream &stream) return true; } -bool Launcher::GameSettings::hasMaster() +bool Config::GameSettings::hasMaster() { bool result = false; QStringList content = mSettings.values(QString("content")); diff --git a/apps/launcher/settings/gamesettings.hpp b/components/config/gamesettings.hpp similarity index 98% rename from apps/launcher/settings/gamesettings.hpp rename to components/config/gamesettings.hpp index 60236200a9..3ec32c1a5c 100644 --- a/apps/launcher/settings/gamesettings.hpp +++ b/components/config/gamesettings.hpp @@ -14,7 +14,7 @@ namespace Files struct ConfigurationManager; } -namespace Launcher +namespace Config { class GameSettings { diff --git a/apps/launcher/settings/launchersettings.cpp b/components/config/launchersettings.cpp similarity index 86% rename from apps/launcher/settings/launchersettings.cpp rename to components/config/launchersettings.cpp index 705453555d..14d66bba79 100644 --- a/apps/launcher/settings/launchersettings.cpp +++ b/components/config/launchersettings.cpp @@ -7,15 +7,15 @@ #include -Launcher::LauncherSettings::LauncherSettings() +Config::LauncherSettings::LauncherSettings() { } -Launcher::LauncherSettings::~LauncherSettings() +Config::LauncherSettings::~LauncherSettings() { } -QStringList Launcher::LauncherSettings::values(const QString &key, Qt::MatchFlags flags) +QStringList Config::LauncherSettings::values(const QString &key, Qt::MatchFlags flags) { QMap settings = SettingsBase::getSettings(); @@ -36,7 +36,7 @@ QStringList Launcher::LauncherSettings::values(const QString &key, Qt::MatchFlag return result; } -QStringList Launcher::LauncherSettings::subKeys(const QString &key) +QStringList Config::LauncherSettings::subKeys(const QString &key) { QMap settings = SettingsBase::getSettings(); QStringList keys = settings.uniqueKeys(); @@ -60,7 +60,7 @@ QStringList Launcher::LauncherSettings::subKeys(const QString &key) return result; } -bool Launcher::LauncherSettings::writeFile(QTextStream &stream) +bool Config::LauncherSettings::writeFile(QTextStream &stream) { QString sectionPrefix; QRegExp sectionRe("([^/]+)/(.+)$"); diff --git a/apps/launcher/settings/launchersettings.hpp b/components/config/launchersettings.hpp similarity index 96% rename from apps/launcher/settings/launchersettings.hpp rename to components/config/launchersettings.hpp index 8acc389a9d..042823ca93 100644 --- a/apps/launcher/settings/launchersettings.hpp +++ b/components/config/launchersettings.hpp @@ -3,7 +3,7 @@ #include "settingsbase.hpp" -namespace Launcher +namespace Config { class LauncherSettings : public SettingsBase > { diff --git a/apps/launcher/settings/settingsbase.hpp b/components/config/settingsbase.hpp similarity index 99% rename from apps/launcher/settings/settingsbase.hpp rename to components/config/settingsbase.hpp index 3a1cf8e30e..92ca34cdfe 100644 --- a/apps/launcher/settings/settingsbase.hpp +++ b/components/config/settingsbase.hpp @@ -7,7 +7,7 @@ #include #include -namespace Launcher +namespace Config { template class SettingsBase From dddd9cba57833124994c19106ad2a32262904bf2 Mon Sep 17 00:00:00 2001 From: pvdk Date: Wed, 25 Dec 2013 15:50:02 +0100 Subject: [PATCH 014/303] Added data path retrieval from openmw.cfg, to use as existing installs --- apps/wizard/CMakeLists.txt | 1 + apps/wizard/mainwizard.cpp | 90 +++++++++++++++++++++++++++++++++----- apps/wizard/mainwizard.hpp | 12 +++++ 3 files changed, 92 insertions(+), 11 deletions(-) diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index 7639cc8e17..4d1563ee75 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -94,6 +94,7 @@ target_link_libraries(openmw-wizard ${Boost_LIBRARIES} ${QT_LIBRARIES} ${LIBUNSHIELD_LIBRARY} + components ) if(DPKG_PROGRAM) diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index fde82e9984..cbc9253edc 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -1,6 +1,10 @@ #include "mainwizard.hpp" #include + +#include +#include +#include #include #include "intropage.hpp" @@ -14,6 +18,7 @@ #include "conclusionpage.hpp" Wizard::MainWizard::MainWizard(QWidget *parent) : + mGameSettings(mCfgMgr), QWizard(parent) { @@ -29,7 +34,6 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setDefaultProperty("QComboBox", "currentText", "currentIndexChanged"); setDefaultProperty("ComponentListWidget", "mCheckedItems", "checkedItemsChanged"); - setDefaultProperty("InstallationListWidget", "mCurrentText", "currentTextChanged"); setupInstallations(); setupPages(); @@ -37,15 +41,47 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : void Wizard::MainWizard::setupInstallations() { - // TODO: detect existing installations - QStringList paths; - paths << QString("/home/pvdk/.wine/drive_c/Program Files/Bethesda Softworks/Morrowind/Data Files"); - paths << QString("/home/pvdk/openmw/Data Files"); - paths << QString("/usr/games/morrowind"); + QString userPath(QFile::decodeName(mCfgMgr.getUserPath().string().c_str())); + QString globalPath(QFile::decodeName(mCfgMgr.getGlobalPath().string().c_str())); - foreach (const QString &path, paths) - { - addInstallation(path); + QStringList paths; + paths.append(userPath + QLatin1String("openmw.cfg")); + paths.append(QLatin1String("openmw.cfg")); + paths.append(globalPath + QLatin1String("openmw.cfg")); + + foreach (const QString &path, paths) { + qDebug() << "Loading config file:" << qPrintable(path); + + QFile file(path); + if (file.exists()) { + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(QObject::tr("
Could not open %0 for reading

\ + Please make sure you have the right permissions \ + and try again.
").arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + QTextStream stream(&file); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + + mGameSettings.readFile(stream); + } + file.close(); + } + + // Check if the paths actually contain data files + foreach (const QString path, mGameSettings.getDataDirs()) { + QDir dir(path); + QStringList filters; + filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; + + // Add to Wizard installations + if (!dir.entryList(filters).isEmpty()) + addInstallation(path); } } @@ -74,6 +110,10 @@ void Wizard::MainWizard::addInstallation(const QString &path) install->iniPath = file.fileName(); mInstallations.insert(QDir::toNativeSeparators(path), install); + + // Add it to the openmw.cfg too + mGameSettings.setMultiValue(QString("data"), path); + mGameSettings.addDataDir(path); } void Wizard::MainWizard::setupPages() @@ -89,6 +129,36 @@ void Wizard::MainWizard::setupPages() setPage(Page_Conclusion, new ConclusionPage(this)); setStartId(Page_Intro); } +void Wizard::MainWizard::accept() +{ + writeSettings(); + QWizard::accept(); +} + +void Wizard::MainWizard::writeSettings() +{ + QString userPath(QFile::decodeName(mCfgMgr.getUserPath().string().c_str())); + QFile file(userPath + QLatin1String("openmw.cfg")); + + if (!file.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) { + // File cannot be opened or 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 %0 for writing

\ + Please make sure you have the right permissions \ + and try again.
").arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + + QTextStream stream(&file); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + + mGameSettings.writeFile(stream); + file.close(); +} bool Wizard::MainWizard::findFiles(const QString &name, const QString &path) { @@ -100,10 +170,8 @@ bool Wizard::MainWizard::findFiles(const QString &name, const QString &path) // TODO: add MIME handling to make sure the files are real if (dir.exists(name + QLatin1String(".esm")) && dir.exists(name + QLatin1String(".bsa"))) { - qDebug() << name << " exists!"; return true; } else { - qDebug() << name << " doesn't exist!"; return false; } diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index c025c729e4..0d4c05fce2 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -4,6 +4,9 @@ #include #include +#include +#include + namespace Wizard { @@ -39,10 +42,19 @@ namespace Wizard QMap mInstallations; + Files::ConfigurationManager mCfgMgr; + private: + void setupInstallations(); void setupPages(); + void writeSettings(); + + Config::GameSettings mGameSettings; + + private slots: + void accept(); }; From 40486370d951fd68a29e30d078e43f2e13c407e4 Mon Sep 17 00:00:00 2001 From: pvdk Date: Wed, 25 Dec 2013 18:52:34 +0100 Subject: [PATCH 015/303] Working on the Unshield functionality --- apps/wizard/installationpage.cpp | 71 ++++++++++++++++++++++++++++- apps/wizard/installationpage.hpp | 6 +++ apps/wizard/unshieldthread.cpp | 61 +++++++++++++++++++++++++ apps/wizard/unshieldthread.hpp | 47 +++++++++++++++++++ files/ui/wizard/installationpage.ui | 6 +-- 5 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 apps/wizard/unshieldthread.cpp create mode 100644 apps/wizard/unshieldthread.hpp diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index e98d75ba4c..bff465feea 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -9,6 +9,7 @@ #include "mainwizard.hpp" #include "inisettings.hpp" +#include "unshieldthread.hpp" Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : QWizardPage(wizard), @@ -37,10 +38,12 @@ void Wizard::InstallationPage::initializePage() } else { - if (components.contains("Tribunal") && mWizard->mInstallations[path]->hasTribunal == false) + if (components.contains(QLatin1String("Tribunal")) + && mWizard->mInstallations[path]->hasTribunal == false) installProgressBar->setMaximum(100); - if (components.contains("Bloodmoon") && mWizard->mInstallations[path]->hasBloodmoon == false) + if (components.contains(QLatin1String("Bloodmoon")) + && mWizard->mInstallations[path]->hasBloodmoon == false) installProgressBar->setMaximum(installProgressBar->maximum() + 100); } @@ -48,6 +51,9 @@ void Wizard::InstallationPage::initializePage() if (field("installation.new").toBool() == false) setupSettings(); + + startInstallation(); + } void Wizard::InstallationPage::setupSettings() @@ -90,6 +96,67 @@ void Wizard::InstallationPage::setupSettings() qDebug() << iniSettings.value("Game Files/GameFile0"); } +void Wizard::InstallationPage::startInstallation() +{ + QStringList components(field("installation.components").toStringList()); + QString path(field("installation.path").toString()); + + UnshieldThread *unshield = new UnshieldThread(); + + connect(unshield, SIGNAL(finished()), + unshield, SLOT(deleteLater())); + + connect(unshield, SIGNAL(finished()), + this, SLOT(installationFinished())); + + connect(unshield, SIGNAL(textChanged(QString)), + installProgressLabel, SLOT(setText(QString))); + + connect(unshield, SIGNAL(textChanged(QString)), + logTextEdit, SLOT(append(QString))); + + if (field("installation.new").toBool() == true) + { + // Always install Morrowind + unshield->setInstallMorrowind(true); + + if (components.contains(QLatin1String("Tribunal"))) + unshield->setInstallTribunal(true); + + if (components.contains(QLatin1String("Bloodmoon"))) + unshield->setInstallBloodmoon(true); + } else { + // Morrowind should already be installed + unshield->setInstallMorrowind(false); + + if (components.contains(QLatin1String("Tribunal")) + && mWizard->mInstallations[path]->hasTribunal == false) + unshield->setInstallTribunal(false); + + if (components.contains(QLatin1String("Bloodmoon")) + && mWizard->mInstallations[path]->hasBloodmoon == false) + unshield->setInstallBloodmoon(true); + } + + + // Set the installation target path + unshield->setPath(path); + + unshield->start(); + +} + +void Wizard::InstallationPage::installationFinished() +{ + qDebug() << "Installation finished!"; + mFinished = true; +} + +bool Wizard::InstallationPage::isComplete() const +{ + return mFinished; +} + int Wizard::InstallationPage::nextId() const { return MainWizard::Page_Import; diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index b4fecc3ba8..022b34c644 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -16,11 +16,17 @@ namespace Wizard InstallationPage(MainWizard *wizard); int nextId() const; + virtual bool isComplete() const; private: MainWizard *mWizard; + bool mFinished; void setupSettings(); + void startInstallation(); + + private slots: + void installationFinished(); protected: void initializePage(); diff --git a/apps/wizard/unshieldthread.cpp b/apps/wizard/unshieldthread.cpp new file mode 100644 index 0000000000..23257e2d82 --- /dev/null +++ b/apps/wizard/unshieldthread.cpp @@ -0,0 +1,61 @@ +#include "unshieldthread.hpp" + +#include +#include + +Wizard::UnshieldThread::UnshieldThread(QObject *parent) : + QThread(parent) +{ + unshield_set_log_level(0); + + mInstallMorrowind = false; + mInstallTribunal = false; + mInstallBloodmoon = false; + +} + +void Wizard::UnshieldThread::setInstallMorrowind(bool install) +{ + mInstallMorrowind = install; +} + +void Wizard::UnshieldThread::setInstallTribunal(bool install) +{ + mInstallTribunal = install; +} + +void Wizard::UnshieldThread::setInstallBloodmoon(bool install) +{ + mInstallBloodmoon = install; +} + +void Wizard::UnshieldThread::setPath(const QString &path) +{ + mPath = path; +} + +void Wizard::UnshieldThread::extract() +{ + emit textChanged(QLatin1String("Starting installation")); + emit textChanged(QLatin1String("Installation target: ") + mPath); + + QStringList components; + if (mInstallMorrowind) + components << QLatin1String("Morrowind"); + + if (mInstallTribunal) + components << QLatin1String("Tribunal"); + + if (mInstallBloodmoon) + components << QLatin1String("Bloodmoon"); + + emit textChanged(QLatin1String("Components: ") + components.join(QLatin1String(", "))); + +} + +void Wizard::UnshieldThread::run() +{ + qDebug() << "From worker thread: " << currentThreadId(); + + extract(); +} diff --git a/apps/wizard/unshieldthread.hpp b/apps/wizard/unshieldthread.hpp new file mode 100644 index 0000000000..764a99e6a9 --- /dev/null +++ b/apps/wizard/unshieldthread.hpp @@ -0,0 +1,47 @@ +#ifndef UNSHIELDTHREAD_HPP +#define UNSHIELDTHREAD_HPP + +#include + +#include + +namespace Wizard +{ + + class UnshieldThread : public QThread + { + Q_OBJECT + public: + explicit UnshieldThread(QObject *parent = 0); + + void setInstallMorrowind(bool install); + void setInstallTribunal(bool install); + void setInstallBloodmoon(bool install); + + void setPath(const QString &path); + + private: + void extract(); + + void extractCab(const QString &cabFile, + const QString &outputDir, bool extractIni); + //void extractFile(Unshield *unshield, + // ) + + bool mInstallMorrowind; + bool mInstallTribunal; + bool mInstallBloodmoon; + + QString mPath; + + protected: + virtual void run(); + + signals: + void textChanged(const QString &text); + + }; + +} + +#endif // UNSHIELDTHREAD_HPP diff --git a/files/ui/wizard/installationpage.ui b/files/ui/wizard/installationpage.ui index 3e94c1e30b..6c1a3c4930 100644 --- a/files/ui/wizard/installationpage.ui +++ b/files/ui/wizard/installationpage.ui @@ -6,8 +6,8 @@ 0 0 - 518 - 423 + 516 + 421
@@ -21,7 +21,7 @@ - + Extracting: %1 From 3a55d88bac1ed3cbb4a10b02a9698f8e74d0cc23 Mon Sep 17 00:00:00 2001 From: pvdk Date: Thu, 26 Dec 2013 18:02:34 +0100 Subject: [PATCH 016/303] Working on the installshield thread --- apps/wizard/installationpage.cpp | 74 ++++-------- apps/wizard/installationpage.hpp | 3 +- apps/wizard/unshieldthread.cpp | 187 ++++++++++++++++++++++++++++++- apps/wizard/unshieldthread.hpp | 22 +++- 4 files changed, 229 insertions(+), 57 deletions(-) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index bff465feea..3ac9b05c83 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -1,11 +1,7 @@ #include "installationpage.hpp" #include -#include -#include -#include #include -#include #include "mainwizard.hpp" #include "inisettings.hpp" @@ -16,6 +12,8 @@ Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : mWizard(wizard) { setupUi(this); + + mFinished = false; } void Wizard::InstallationPage::initializePage() @@ -49,51 +47,7 @@ void Wizard::InstallationPage::initializePage() installProgressBar->setValue(100); - if (field("installation.new").toBool() == false) - setupSettings(); - startInstallation(); - -} - -void Wizard::InstallationPage::setupSettings() -{ - // Test settings - IniSettings iniSettings; - - QString path(field("installation.path").toString()); - QFile file(mWizard->mInstallations[path]->iniPath); - - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error opening Morrowind configuration file")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); - msgBox.exec(); - mWizard->close(); - return; - } - - QTextStream stream(&file); - - QString language(field("installation.language").toString()); - - if (language == QLatin1String("Polish")) { - stream.setCodec(QTextCodec::codecForName("windows-1250")); - } - else if (language == QLatin1String("Russian")) { - stream.setCodec(QTextCodec::codecForName("windows-1251")); - } - else { - stream.setCodec(QTextCodec::codecForName("windows-1252")); - } - - iniSettings.readFile(stream); - - qDebug() << iniSettings.value("Game Files/GameFile0"); } void Wizard::InstallationPage::startInstallation() @@ -115,6 +69,9 @@ void Wizard::InstallationPage::startInstallation() connect(unshield, SIGNAL(textChanged(QString)), logTextEdit, SLOT(append(QString))); + connect(unshield, SIGNAL(progressChanged(int)), + installProgressBar, SLOT(setValue(int))); + if (field("installation.new").toBool() == true) { // Always install Morrowind @@ -131,25 +88,40 @@ void Wizard::InstallationPage::startInstallation() if (components.contains(QLatin1String("Tribunal")) && mWizard->mInstallations[path]->hasTribunal == false) - unshield->setInstallTribunal(false); + unshield->setInstallTribunal(true); if (components.contains(QLatin1String("Bloodmoon")) && mWizard->mInstallations[path]->hasBloodmoon == false) unshield->setInstallBloodmoon(true); - } + // Set the location of the Morrowind.ini to update + unshield->setIniPath(mWizard->mInstallations[path]->iniPath); + } // Set the installation target path unshield->setPath(path); + // Set the right codec to use for Morrowind.ini + QString language(field("installation.language").toString()); + + if (language == QLatin1String("Polish")) { + unshield->setIniCodec(QTextCodec::codecForName("windows-1250")); + } + else if (language == QLatin1String("Russian")) { + unshield->setIniCodec(QTextCodec::codecForName("windows-1251")); + } + else { + unshield->setIniCodec(QTextCodec::codecForName("windows-1252")); + } + unshield->start(); } void Wizard::InstallationPage::installationFinished() { - qDebug() << "Installation finished!"; mFinished = true; + emit completeChanged(); } bool Wizard::InstallationPage::isComplete() const diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index 022b34c644..42b2be819a 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -4,10 +4,12 @@ #include #include "ui_installationpage.h" +#include "inisettings.hpp" namespace Wizard { class MainWizard; + class IniSettings; class InstallationPage : public QWizardPage, private Ui::InstallationPage { @@ -22,7 +24,6 @@ namespace Wizard MainWizard *mWizard; bool mFinished; - void setupSettings(); void startInstallation(); private slots: diff --git a/apps/wizard/unshieldthread.cpp b/apps/wizard/unshieldthread.cpp index 23257e2d82..d759b56a6c 100644 --- a/apps/wizard/unshieldthread.cpp +++ b/apps/wizard/unshieldthread.cpp @@ -1,13 +1,28 @@ #include "unshieldthread.hpp" #include + +#include #include +#include +#include +#include +#include + +#include Wizard::UnshieldThread::UnshieldThread(QObject *parent) : - QThread(parent) + QThread(parent), + mIniSettings() { unshield_set_log_level(0); + mPath = QString(); + mIniPath = QString(); + + // Default to Latin encoding + mIniCodec = QTextCodec::codecForName("windows-1252"); + mInstallMorrowind = false; mInstallTribunal = false; mInstallBloodmoon = false; @@ -34,6 +49,43 @@ void Wizard::UnshieldThread::setPath(const QString &path) mPath = path; } +void Wizard::UnshieldThread::setIniPath(const QString &path) +{ + mIniPath = path; +} + +void Wizard::UnshieldThread::setIniCodec(QTextCodec *codec) +{ + mIniCodec = codec; +} + +void Wizard::UnshieldThread::setupSettings() +{ + // Create Morrowind.ini settings map + if (mIniPath.isEmpty()) + return; + + QFile file(mIniPath); + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + // TODO: Emit error signal + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening Morrowind configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(QObject::tr("
Could not open %0 for reading

\ + Please make sure you have the right permissions \ + and try again.
").arg(file.fileName())); + msgBox.exec(); + return; + } + + QTextStream stream(&file); + stream.setCodec(mIniCodec); + + mIniSettings.readFile(stream); +} + void Wizard::UnshieldThread::extract() { emit textChanged(QLatin1String("Starting installation")); @@ -51,11 +103,144 @@ void Wizard::UnshieldThread::extract() emit textChanged(QLatin1String("Components: ") + components.join(QLatin1String(", "))); + emit textChanged(QLatin1String("Updating Morrowind.ini: ") + mIniPath); + + //emit progressChanged(45); + + /// +// bfs::path outputDataFilesDir = mOutputPath; +// outputDataFilesDir /= "Data Files"; +// bfs::path extractPath = mOutputPath; +// extractPath /= "extract-temp"; + + // Create temporary extract directory + // TODO: Use QTemporaryDir in Qt 5.0 + QString tempPath(mPath + QLatin1String("/extract-temp")); + QDir dir; + dir.mkpath(tempPath); + + if (mInstallMorrowind) + { + QString morrowindTempPath(tempPath + QLatin1String("/morrowind")); + QString morrowindCab(QLatin1String("/mnt/cdrom/data1.hdr")); + + extractCab(morrowindCab, morrowindTempPath); + + // TODO: Throw error; + // Move the files from the temporary path to the destination folder + + qDebug() << "rename: " << morrowindTempPath << " to: " << mPath; + if (!dir.rename(morrowindTempPath, mPath)) + qDebug() << "failed!"; + + } + +// if(!mMorrowindDone && mMorrowindPath.string().length() > 0) +// { +// mMorrowindDone = true; + +// bfs::path mwExtractPath = extractPath / "morrowind"; +// extract_cab(mMorrowindPath, mwExtractPath, true); + +// bfs::path dFilesDir = findFile(mwExtractPath, "morrowind.esm").parent_path(); + +// installToPath(dFilesDir, outputDataFilesDir); + +// install_dfiles_outside(mwExtractPath, outputDataFilesDir); + +// // Videos are often kept uncompressed on the cd +// bfs::path videosPath = findFile(mMorrowindPath.parent_path(), "video", false); +// if(videosPath.string() != "") +// { +// emit signalGUI(QString("Installing Videos...")); +// installToPath(videosPath, outputDataFilesDir / "Video", true); +// } + +// bfs::path cdDFiles = findFile(mMorrowindPath.parent_path(), "data files", false); +// if(cdDFiles.string() != "") +// { +// emit signalGUI(QString("Installing Uncompressed Data files from CD...")); +// installToPath(cdDFiles, outputDataFilesDir, true); +// } + + +// bfs::rename(findFile(mwExtractPath, "morrowind.ini"), outputDataFilesDir / "Morrowind.ini"); + +// mTribunalDone = contains(outputDataFilesDir, "tribunal.esm"); +// mBloodmoonDone = contains(outputDataFilesDir, "bloodmoon.esm"); + +// } + + + + /// + +} + +bool Wizard::UnshieldThread::extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter) +{ + bool success; + QString path(outputDir); + path.append('/'); + + int directory = unshield_file_directory(unshield, index); + + if (!prefix.isEmpty()) + path.append(prefix + QLatin1Char('/')); + + if (directory >= 0) + path.append(QString::fromLatin1(unshield_directory_name(unshield, directory)) + QLatin1Char('/')); + + // Ensure the target path exists + QDir dir; + dir.mkpath(path); + + QString fileName(path); + fileName.append(QString::fromLatin1(unshield_file_name(unshield, index))); + + // Calculate the percentage done + int progress = qCeil(((float) counter / (float) unshield_file_count(unshield)) * 100); + + qDebug() << progress << counter << unshield_file_count(unshield); + + emit textChanged(QLatin1String("Extracting: ") + QString::fromLatin1(unshield_file_name(unshield, index))); + emit progressChanged(progress); + + success = unshield_file_save(unshield, index, fileName.toLatin1().constData()); + + if (!success) + dir.remove(fileName); + + return success; +} + +void Wizard::UnshieldThread::extractCab(const QString &cabFile, const QString &outputDir) +{ + Unshield *unshield; + unshield = unshield_open(cabFile.toLatin1().constData()); + + int counter = 0; + + for (int i=0; ifirst_file; j<=group->last_file; ++j) + { + if (unshield_file_is_valid(unshield, j)) { + extractFile(unshield, outputDir, group->name, j, counter); + ++counter; + } + } + } + + unshield_close(unshield); } void Wizard::UnshieldThread::run() { qDebug() << "From worker thread: " << currentThreadId(); + setupSettings(); extract(); } diff --git a/apps/wizard/unshieldthread.hpp b/apps/wizard/unshieldthread.hpp index 764a99e6a9..7f7250875b 100644 --- a/apps/wizard/unshieldthread.hpp +++ b/apps/wizard/unshieldthread.hpp @@ -5,6 +5,10 @@ #include +#include "inisettings.hpp" + +class QTextCodec; + namespace Wizard { @@ -19,26 +23,36 @@ namespace Wizard void setInstallBloodmoon(bool install); void setPath(const QString &path); + void setIniPath(const QString &path); + + void setIniCodec(QTextCodec *codec); private: + + void setupSettings(); void extract(); - void extractCab(const QString &cabFile, - const QString &outputDir, bool extractIni); - //void extractFile(Unshield *unshield, - // ) + void extractCab(const QString &cabFile, const QString &outputDir); + bool extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter); + bool mInstallMorrowind; bool mInstallTribunal; bool mInstallBloodmoon; QString mPath; + QString mIniPath; + + IniSettings mIniSettings; + + QTextCodec *mIniCodec; protected: virtual void run(); signals: void textChanged(const QString &text); + void progressChanged(int progress); }; From a2ca7b92a227530579c442cf553631dcccdb7303 Mon Sep 17 00:00:00 2001 From: pvdk Date: Wed, 1 Jan 2014 17:15:34 +0100 Subject: [PATCH 017/303] Working on the unshield implementation, added support for moving temp files --- apps/wizard/unshieldthread.cpp | 140 +++++++++++++++++++++++++++++++-- apps/wizard/unshieldthread.hpp | 5 ++ 2 files changed, 140 insertions(+), 5 deletions(-) diff --git a/apps/wizard/unshieldthread.cpp b/apps/wizard/unshieldthread.cpp index d759b56a6c..0f8dbc4771 100644 --- a/apps/wizard/unshieldthread.cpp +++ b/apps/wizard/unshieldthread.cpp @@ -2,6 +2,8 @@ #include +#include +#include #include #include #include @@ -86,6 +88,91 @@ void Wizard::UnshieldThread::setupSettings() mIniSettings.readFile(stream); } +bool Wizard::UnshieldThread::removeDirectory(const QString &dirName) +{ + bool result = true; + QDir dir(dirName); + + if (dir.exists(dirName)) + { + QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | + QDir::System | QDir::Hidden | + QDir::AllDirs | QDir::Files, QDir::DirsFirst)); + foreach(QFileInfo info, list) { + if (info.isDir()) { + result = removeDirectory(info.absoluteFilePath()); + } else { + result = QFile::remove(info.absoluteFilePath()); + } + + if (!result) + return result; + } + + result = dir.rmdir(dirName); + } + return result; +} + +bool Wizard::UnshieldThread::moveFile(const QString &source, const QString &destination) +{ + QDir dir; + QFile file; + + if (dir.rename(source, destination)) { + return true; + } else { + if (file.copy(source, destination)) { + return file.remove(source); + } else { + qDebug() << "copy failed! " << file.errorString(); + } + } + + return false; +} + +bool Wizard::UnshieldThread::moveDirectory(const QString &source, const QString &destination) +{ + QDir sourceDir(source); + QDir destDir(destination); + + if (!destDir.exists()) { + sourceDir.mkpath(destination); + destDir.refresh(); + } + + if (!destDir.exists()) + return false; + + bool result = true; + + QFileInfoList list(sourceDir.entryInfoList(QDir::NoDotAndDotDot | + QDir::System | QDir::Hidden | + QDir::AllDirs | QDir::Files, QDir::DirsFirst)); + + foreach (const QFileInfo &info, list) { + QString relativePath(info.absoluteFilePath()); + relativePath.remove(source); + + if (info.isSymLink()) + continue; + + if (info.isDir()) { + result = moveDirectory(info.absoluteFilePath(), destDir.absolutePath() + relativePath); + } else { + qDebug() << "moving: " << info.absoluteFilePath() << " to: " << destDir.absolutePath() + relativePath; + + result = moveFile(info.absoluteFilePath(), destDir.absolutePath() + relativePath); + } + + //if (!result) + // return result; + } + + return result && removeDirectory(sourceDir.absolutePath()); +} + void Wizard::UnshieldThread::extract() { emit textChanged(QLatin1String("Starting installation")); @@ -129,10 +216,48 @@ void Wizard::UnshieldThread::extract() // TODO: Throw error; // Move the files from the temporary path to the destination folder - qDebug() << "rename: " << morrowindTempPath << " to: " << mPath; - if (!dir.rename(morrowindTempPath, mPath)) + //qDebug() << "rename: " << morrowindTempPath << " to: " << mPath; + morrowindTempPath.append(QDir::separator() + QLatin1String("Data Files")); + if (!moveDirectory(morrowindTempPath, mPath)) qDebug() << "failed!"; + QDir source(QLatin1String("/mnt/cdrom/")); + QStringList directories; + directories << QLatin1String("Fonts") + << QLatin1String("Music") + << QLatin1String("Sound") + << QLatin1String("Splash"); + + QFileInfoList list(sourceDir.entryInfoList(QDir::NoDotAndDotDot | + QDir::System | QDir::Hidden | + QDir::AllDirs)); + + + foreach(QFileInfo info, list) { + if (info.isSymLink()) + continue; + + if (directories.contains(info.fileName())) + copyDirectory(info.absoluteFilePath(), mPath); + } + + + bfs::path fonts = findFile(from, "fonts", false); + if(fonts.string() != "") + installToPath(fonts, dFiles / "Fonts"); + + bfs::path music = findFile(from, "music", false); + if(music.string() != "") + installToPath(music, dFiles / "Music"); + + bfs::path sound = findFile(from, "sound", false); + if(sound.string() != "") + installToPath(sound, dFiles / "Sound"); + + bfs::path splash = findFile(from, "splash", false); + if(splash.string() != "") + installToPath(splash, dFiles / "Splash"); + } // if(!mMorrowindDone && mMorrowindPath.string().length() > 0) @@ -181,16 +306,21 @@ bool Wizard::UnshieldThread::extractFile(Unshield *unshield, const QString &outp { bool success; QString path(outputDir); - path.append('/'); + path.append(QDir::separator()); int directory = unshield_file_directory(unshield, index); if (!prefix.isEmpty()) - path.append(prefix + QLatin1Char('/')); + path.append(prefix + QDir::separator()); if (directory >= 0) - path.append(QString::fromLatin1(unshield_directory_name(unshield, directory)) + QLatin1Char('/')); + path.append(QString::fromLatin1(unshield_directory_name(unshield, directory)) + QDir::separator()); + // Ensure the path has the right separators + path.replace(QLatin1Char('\\'), QDir::separator()); + path = QDir::toNativeSeparators(path); + + qDebug() << "path is: " << path << QString::fromLatin1(unshield_directory_name(unshield, directory)) + QLatin1Char('/'); // Ensure the target path exists QDir dir; dir.mkpath(path); diff --git a/apps/wizard/unshieldthread.hpp b/apps/wizard/unshieldthread.hpp index 7f7250875b..cc3740ad82 100644 --- a/apps/wizard/unshieldthread.hpp +++ b/apps/wizard/unshieldthread.hpp @@ -29,6 +29,11 @@ namespace Wizard private: + bool removeDirectory(const QString &dirName); + + bool moveFile(const QString &source, const QString &destination); + bool moveDirectory(const QString &source, const QString &destination); + void setupSettings(); void extract(); From 691e007524591eec04d43fb8faabc38cc0046973 Mon Sep 17 00:00:00 2001 From: pvdk Date: Wed, 1 Jan 2014 22:46:29 +0100 Subject: [PATCH 018/303] Redid the unshield thread stuff, this time the way it should be done --- apps/wizard/CMakeLists.txt | 9 ++- apps/wizard/installationpage.cpp | 19 ++++- .../unshieldworker.cpp} | 79 ++++++++----------- .../unshieldworker.hpp} | 28 ++++--- 4 files changed, 69 insertions(+), 66 deletions(-) rename apps/wizard/{unshieldthread.cpp => unshield/unshieldworker.cpp} (82%) rename apps/wizard/{unshieldthread.hpp => unshield/unshieldworker.hpp} (78%) diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index 4d1563ee75..f2defd031b 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -11,7 +11,8 @@ set(WIZARD main.cpp mainwizard.cpp methodselectionpage.cpp - unshieldthread.cpp + + unshield/unshieldworker.cpp utils/componentlistwidget.cpp ) @@ -28,7 +29,8 @@ set(WIZARD_HEADER languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp - unshieldthread.hpp + + unshield/unshieldworker.hpp utils/componentlistwidget.hpp ) @@ -45,7 +47,8 @@ set(WIZARD_HEADER_MOC languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp - unshieldthread.hpp + + unshield/unshieldworker.hpp utils/componentlistwidget.hpp ) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 3ac9b05c83..374c8180c6 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -5,7 +5,7 @@ #include "mainwizard.hpp" #include "inisettings.hpp" -#include "unshieldthread.hpp" +#include "unshield/unshieldworker.hpp" Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : QWizardPage(wizard), @@ -55,11 +55,23 @@ void Wizard::InstallationPage::startInstallation() QStringList components(field("installation.components").toStringList()); QString path(field("installation.path").toString()); - UnshieldThread *unshield = new UnshieldThread(); + QThread* thread = new QThread(); + UnshieldWorker* unshield = new UnshieldWorker(); + + unshield->moveToThread(thread); + + connect(thread, SIGNAL(started()), + unshield, SLOT(extract())); + + connect(unshield, SIGNAL(finished()), + thread, SLOT(quit())); connect(unshield, SIGNAL(finished()), unshield, SLOT(deleteLater())); + connect(unshield, SIGNAL(finished()), + thread, SLOT(deleteLater())); + connect(unshield, SIGNAL(finished()), this, SLOT(installationFinished())); @@ -114,12 +126,13 @@ void Wizard::InstallationPage::startInstallation() unshield->setIniCodec(QTextCodec::codecForName("windows-1252")); } - unshield->start(); + thread->start(); } void Wizard::InstallationPage::installationFinished() { + qDebug() << "finished!"; mFinished = true; emit completeChanged(); } diff --git a/apps/wizard/unshieldthread.cpp b/apps/wizard/unshield/unshieldworker.cpp similarity index 82% rename from apps/wizard/unshieldthread.cpp rename to apps/wizard/unshield/unshieldworker.cpp index 0f8dbc4771..71beaf9b8a 100644 --- a/apps/wizard/unshieldthread.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -1,4 +1,4 @@ -#include "unshieldthread.hpp" +#include "unshieldworker.hpp" #include @@ -10,11 +10,12 @@ #include #include #include +#include #include -Wizard::UnshieldThread::UnshieldThread(QObject *parent) : - QThread(parent), +Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : + QObject(parent), mIniSettings() { unshield_set_log_level(0); @@ -28,40 +29,44 @@ Wizard::UnshieldThread::UnshieldThread(QObject *parent) : mInstallMorrowind = false; mInstallTribunal = false; mInstallBloodmoon = false; +} + +Wizard::UnshieldWorker::~UnshieldWorker() +{ } -void Wizard::UnshieldThread::setInstallMorrowind(bool install) +void Wizard::UnshieldWorker::setInstallMorrowind(bool install) { mInstallMorrowind = install; } -void Wizard::UnshieldThread::setInstallTribunal(bool install) +void Wizard::UnshieldWorker::setInstallTribunal(bool install) { mInstallTribunal = install; } -void Wizard::UnshieldThread::setInstallBloodmoon(bool install) +void Wizard::UnshieldWorker::setInstallBloodmoon(bool install) { mInstallBloodmoon = install; } -void Wizard::UnshieldThread::setPath(const QString &path) +void Wizard::UnshieldWorker::setPath(const QString &path) { mPath = path; } -void Wizard::UnshieldThread::setIniPath(const QString &path) +void Wizard::UnshieldWorker::setIniPath(const QString &path) { mIniPath = path; } -void Wizard::UnshieldThread::setIniCodec(QTextCodec *codec) +void Wizard::UnshieldWorker::setIniCodec(QTextCodec *codec) { mIniCodec = codec; } -void Wizard::UnshieldThread::setupSettings() +void Wizard::UnshieldWorker::setupSettings() { // Create Morrowind.ini settings map if (mIniPath.isEmpty()) @@ -88,7 +93,7 @@ void Wizard::UnshieldThread::setupSettings() mIniSettings.readFile(stream); } -bool Wizard::UnshieldThread::removeDirectory(const QString &dirName) +bool Wizard::UnshieldWorker::removeDirectory(const QString &dirName) { bool result = true; QDir dir(dirName); @@ -114,7 +119,7 @@ bool Wizard::UnshieldThread::removeDirectory(const QString &dirName) return result; } -bool Wizard::UnshieldThread::moveFile(const QString &source, const QString &destination) +bool Wizard::UnshieldWorker::moveFile(const QString &source, const QString &destination) { QDir dir; QFile file; @@ -132,7 +137,7 @@ bool Wizard::UnshieldThread::moveFile(const QString &source, const QString &dest return false; } -bool Wizard::UnshieldThread::moveDirectory(const QString &source, const QString &destination) +bool Wizard::UnshieldWorker::moveDirectory(const QString &source, const QString &destination) { QDir sourceDir(source); QDir destDir(destination); @@ -173,7 +178,7 @@ bool Wizard::UnshieldThread::moveDirectory(const QString &source, const QString return result && removeDirectory(sourceDir.absolutePath()); } -void Wizard::UnshieldThread::extract() +void Wizard::UnshieldWorker::extract() { emit textChanged(QLatin1String("Starting installation")); emit textChanged(QLatin1String("Installation target: ") + mPath); @@ -211,22 +216,23 @@ void Wizard::UnshieldThread::extract() QString morrowindTempPath(tempPath + QLatin1String("/morrowind")); QString morrowindCab(QLatin1String("/mnt/cdrom/data1.hdr")); - extractCab(morrowindCab, morrowindTempPath); + //extractCab(morrowindCab, morrowindTempPath); // TODO: Throw error; // Move the files from the temporary path to the destination folder //qDebug() << "rename: " << morrowindTempPath << " to: " << mPath; morrowindTempPath.append(QDir::separator() + QLatin1String("Data Files")); - if (!moveDirectory(morrowindTempPath, mPath)) - qDebug() << "failed!"; +// if (!moveDirectory(morrowindTempPath, mPath)) +// qDebug() << "failed!"; - QDir source(QLatin1String("/mnt/cdrom/")); + QDir sourceDir(QLatin1String("/mnt/cdrom/")); QStringList directories; directories << QLatin1String("Fonts") << QLatin1String("Music") << QLatin1String("Sound") - << QLatin1String("Splash"); + << QLatin1String("Splash") + << QLatin1String("Video"); QFileInfoList list(sourceDir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | @@ -237,27 +243,13 @@ void Wizard::UnshieldThread::extract() if (info.isSymLink()) continue; + qDebug() << "not found " << info.fileName(); + if (directories.contains(info.fileName())) - copyDirectory(info.absoluteFilePath(), mPath); + qDebug() << "found " << info.fileName(); +// copyDirectory(info.absoluteFilePath(), mPath); } - - bfs::path fonts = findFile(from, "fonts", false); - if(fonts.string() != "") - installToPath(fonts, dFiles / "Fonts"); - - bfs::path music = findFile(from, "music", false); - if(music.string() != "") - installToPath(music, dFiles / "Music"); - - bfs::path sound = findFile(from, "sound", false); - if(sound.string() != "") - installToPath(sound, dFiles / "Sound"); - - bfs::path splash = findFile(from, "splash", false); - if(splash.string() != "") - installToPath(splash, dFiles / "Splash"); - } // if(!mMorrowindDone && mMorrowindPath.string().length() > 0) @@ -299,10 +291,11 @@ void Wizard::UnshieldThread::extract() /// + emit finished(); } -bool Wizard::UnshieldThread::extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter) +bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter) { bool success; QString path(outputDir); @@ -344,7 +337,7 @@ bool Wizard::UnshieldThread::extractFile(Unshield *unshield, const QString &outp return success; } -void Wizard::UnshieldThread::extractCab(const QString &cabFile, const QString &outputDir) +void Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &outputDir) { Unshield *unshield; unshield = unshield_open(cabFile.toLatin1().constData()); @@ -366,11 +359,3 @@ void Wizard::UnshieldThread::extractCab(const QString &cabFile, const QString &o unshield_close(unshield); } - -void Wizard::UnshieldThread::run() -{ - qDebug() << "From worker thread: " << currentThreadId(); - - setupSettings(); - extract(); -} diff --git a/apps/wizard/unshieldthread.hpp b/apps/wizard/unshield/unshieldworker.hpp similarity index 78% rename from apps/wizard/unshieldthread.hpp rename to apps/wizard/unshield/unshieldworker.hpp index cc3740ad82..61e021865f 100644 --- a/apps/wizard/unshieldthread.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -1,22 +1,22 @@ -#ifndef UNSHIELDTHREAD_HPP -#define UNSHIELDTHREAD_HPP +#ifndef UNSHIELDWORKER_HPP +#define UNSHIELDWORKER_HPP +#include #include #include -#include "inisettings.hpp" - -class QTextCodec; +#include "../inisettings.hpp" namespace Wizard { - - class UnshieldThread : public QThread + class UnshieldWorker : public QObject { Q_OBJECT + public: - explicit UnshieldThread(QObject *parent = 0); + UnshieldWorker(QObject *parent = 0); + ~UnshieldWorker(); void setInstallMorrowind(bool install); void setInstallTribunal(bool install); @@ -35,7 +35,6 @@ namespace Wizard bool moveDirectory(const QString &source, const QString &destination); void setupSettings(); - void extract(); void extractCab(const QString &cabFile, const QString &outputDir); bool extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter); @@ -52,15 +51,18 @@ namespace Wizard QTextCodec *mIniCodec; - protected: - virtual void run(); + + public slots: + void extract(); signals: + void finished(); void textChanged(const QString &text); + void error(const QString &text); void progressChanged(int progress); - }; + }; } -#endif // UNSHIELDTHREAD_HPP +#endif // UNSHIELDWORKER_HPP From 0f7f3391f7528ec01882d433ac369f8195eb3e64 Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 17 Jan 2014 13:21:44 +0100 Subject: [PATCH 019/303] WIP: Working on the installation of addons --- apps/wizard/installationpage.cpp | 11 +- apps/wizard/installationpage.hpp | 1 + apps/wizard/unshield/unshieldworker.cpp | 285 ++++++++++++++++-------- apps/wizard/unshield/unshieldworker.hpp | 11 + files/ui/wizard/installationpage.ui | 6 +- 5 files changed, 220 insertions(+), 94 deletions(-) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 374c8180c6..045138ab71 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -45,8 +45,6 @@ void Wizard::InstallationPage::initializePage() installProgressBar->setMaximum(installProgressBar->maximum() + 100); } - installProgressBar->setValue(100); - startInstallation(); } @@ -75,6 +73,9 @@ void Wizard::InstallationPage::startInstallation() connect(unshield, SIGNAL(finished()), this, SLOT(installationFinished())); + connect(unshield, SIGNAL(error(QString)), + this, SLOT(installationError(QString))); + connect(unshield, SIGNAL(textChanged(QString)), installProgressLabel, SLOT(setText(QString))); @@ -135,6 +136,12 @@ void Wizard::InstallationPage::installationFinished() qDebug() << "finished!"; mFinished = true; emit completeChanged(); + +} + +void Wizard::InstallationPage::installationError(const QString &text) +{ + qDebug() << "error: " << text; } bool Wizard::InstallationPage::isComplete() const diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index 42b2be819a..f55ab8ab5b 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -28,6 +28,7 @@ namespace Wizard private slots: void installationFinished(); + void installationError(const QString &text); protected: void initializePage(); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 71beaf9b8a..4f92b1cf9b 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -29,6 +29,10 @@ Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : mInstallMorrowind = false; mInstallTribunal = false; mInstallBloodmoon = false; + + mMorrowindDone = false; + mTribunalDone = false; + mBloodmoonDone = false; } Wizard::UnshieldWorker::~UnshieldWorker() @@ -119,25 +123,30 @@ bool Wizard::UnshieldWorker::removeDirectory(const QString &dirName) return result; } -bool Wizard::UnshieldWorker::moveFile(const QString &source, const QString &destination) +bool Wizard::UnshieldWorker::copyFile(const QString &source, const QString &destination, bool keepSource) { QDir dir; QFile file; - if (dir.rename(source, destination)) { - return true; - } else { - if (file.copy(source, destination)) { + QFileInfo info(destination); + + if (info.exists()) + dir.remove(info.absoluteFilePath()); + + if (file.copy(source, destination)) { + if (!keepSource) { return file.remove(source); } else { - qDebug() << "copy failed! " << file.errorString(); + return true; } + } else { + qDebug() << "copy failed! " << file.errorString(); } return false; } -bool Wizard::UnshieldWorker::moveDirectory(const QString &source, const QString &destination) +bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString &destination, bool keepSource) { QDir sourceDir(source); QDir destDir(destination); @@ -170,12 +179,51 @@ bool Wizard::UnshieldWorker::moveDirectory(const QString &source, const QString result = moveFile(info.absoluteFilePath(), destDir.absolutePath() + relativePath); } - - //if (!result) - // return result; } - return result && removeDirectory(sourceDir.absolutePath()); + if (!keepSource) + return result && removeDirectory(sourceDir.absolutePath()); + + return result; +} + +bool Wizard::UnshieldWorker::moveFile(const QString &source, const QString &destination) +{ + return copyFile(source, destination, false); +} + +bool Wizard::UnshieldWorker::moveDirectory(const QString &source, const QString &destination) +{ + return copyDirectory(source, destination, false); +} + +void Wizard::UnshieldWorker::installDirectories(const QString &source) +{ + QDir dir(source); + + if (!dir.exists()) + return; + + QStringList directories; + directories << QLatin1String("Fonts") + << QLatin1String("Music") + << QLatin1String("Sound") + << QLatin1String("Splash") + << QLatin1String("Video"); + + QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | + QDir::System | QDir::Hidden | + QDir::AllDirs)); + foreach(QFileInfo info, list) { + if (info.isSymLink()) + continue; + + if (directories.contains(info.fileName())) { + qDebug() << "found " << info.fileName(); + emit textChanged(tr("Extracting: %1 directory").arg(info.fileName())); + copyDirectory(info.absoluteFilePath(), mPath + QDir::separator() + info.fileName()); + } + } } void Wizard::UnshieldWorker::extract() @@ -183,114 +231,165 @@ void Wizard::UnshieldWorker::extract() emit textChanged(QLatin1String("Starting installation")); emit textChanged(QLatin1String("Installation target: ") + mPath); - QStringList components; - if (mInstallMorrowind) - components << QLatin1String("Morrowind"); - - if (mInstallTribunal) - components << QLatin1String("Tribunal"); - - if (mInstallBloodmoon) - components << QLatin1String("Bloodmoon"); - - emit textChanged(QLatin1String("Components: ") + components.join(QLatin1String(", "))); - - emit textChanged(QLatin1String("Updating Morrowind.ini: ") + mIniPath); - - //emit progressChanged(45); - - /// -// bfs::path outputDataFilesDir = mOutputPath; -// outputDataFilesDir /= "Data Files"; -// bfs::path extractPath = mOutputPath; -// extractPath /= "extract-temp"; + QString diskPath("/mnt/cdrom/"); + QDir disk(diskPath); // Create temporary extract directory // TODO: Use QTemporaryDir in Qt 5.0 - QString tempPath(mPath + QLatin1String("/extract-temp")); - QDir dir; - dir.mkpath(tempPath); + QString tempPath(mPath + QDir::separator() + QLatin1String("extract-temp")); + QDir temp; + + // Make sure the temporary folder is empty + removeDirectory(tempPath); + + if (!temp.mkpath(tempPath)) { + qDebug() << "Can't make path"; + return; + } + + temp.setPath(tempPath); + disk.setPath(diskPath); if (mInstallMorrowind) { - QString morrowindTempPath(tempPath + QLatin1String("/morrowind")); - QString morrowindCab(QLatin1String("/mnt/cdrom/data1.hdr")); + emit textChanged(QLatin1String("Installing Morrowind\n")); - //extractCab(morrowindCab, morrowindTempPath); + if (!temp.mkdir(QLatin1String("morrowind"))) { + qDebug() << "Can't make dir"; + return; + } + + if (!temp.cd(QLatin1String("morrowind"))) { + qDebug() << "Can't cd to dir"; + return; + } + + if (!disk.exists(QLatin1String("data1.hdr"))) { + qDebug() << "No data found!"; + return; + } + + // Extract the installation files + extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); // TODO: Throw error; // Move the files from the temporary path to the destination folder //qDebug() << "rename: " << morrowindTempPath << " to: " << mPath; - morrowindTempPath.append(QDir::separator() + QLatin1String("Data Files")); -// if (!moveDirectory(morrowindTempPath, mPath)) -// qDebug() << "failed!"; + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) + qDebug() << "failed!"; - QDir sourceDir(QLatin1String("/mnt/cdrom/")); - QStringList directories; - directories << QLatin1String("Fonts") - << QLatin1String("Music") - << QLatin1String("Sound") - << QLatin1String("Splash") - << QLatin1String("Video"); + // Install files outside of cab archives + qDebug() << temp.absolutePath() << disk.absolutePath(); + installDirectories(disk.absolutePath()); - QFileInfoList list(sourceDir.entryInfoList(QDir::NoDotAndDotDot | - QDir::System | QDir::Hidden | - QDir::AllDirs)); + // Copy Morrowind configuration file + QString iniPath(temp.absoluteFilePath(QLatin1String("App Executables"))); + iniPath.append(QDir::separator() + QLatin1String("Morrowind.ini")); + QFileInfo info(iniPath); - foreach(QFileInfo info, list) { - if (info.isSymLink()) - continue; + qDebug() << info.absoluteFilePath() << mPath; - qDebug() << "not found " << info.fileName(); - - if (directories.contains(info.fileName())) - qDebug() << "found " << info.fileName(); -// copyDirectory(info.absoluteFilePath(), mPath); + if (info.exists()) { + emit textChanged(tr("Extracting: Morrowind.ini")); + moveFile(info.absoluteFilePath(), mPath + QDir::separator() + QLatin1String("Morrowind.ini")); + } else { + qDebug() << "Could not find ini file!"; } + mMorrowindDone = true; + } -// if(!mMorrowindDone && mMorrowindPath.string().length() > 0) -// { -// mMorrowindDone = true; + temp.setPath(tempPath); + disk.setPath(diskPath); -// bfs::path mwExtractPath = extractPath / "morrowind"; -// extract_cab(mMorrowindPath, mwExtractPath, true); + if (mInstallTribunal) + { + emit textChanged(QLatin1String("Installing Tribunal\n")); -// bfs::path dFilesDir = findFile(mwExtractPath, "morrowind.esm").parent_path(); + if (!temp.mkdir(QLatin1String("tribunal"))) { + qDebug() << "Can't make dir"; + return; + } -// installToPath(dFilesDir, outputDataFilesDir); + if (!temp.cd(QLatin1String("tribunal"))) { + qDebug() << "Can't cd to dir"; + return; + } -// install_dfiles_outside(mwExtractPath, outputDataFilesDir); + if (!disk.cd(QLatin1String("Tribunal"))) + qDebug() << "Show file selector"; -// // Videos are often kept uncompressed on the cd -// bfs::path videosPath = findFile(mMorrowindPath.parent_path(), "video", false); -// if(videosPath.string() != "") -// { -// emit signalGUI(QString("Installing Videos...")); -// installToPath(videosPath, outputDataFilesDir / "Video", true); -// } + if (!disk.exists(QLatin1String("data1.hdr"))) { + qDebug() << "No data found!"; + return; + } -// bfs::path cdDFiles = findFile(mMorrowindPath.parent_path(), "data files", false); -// if(cdDFiles.string() != "") -// { -// emit signalGUI(QString("Installing Uncompressed Data files from CD...")); -// installToPath(cdDFiles, outputDataFilesDir, true); -// } + // Extract the installation files + extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); + + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) + qDebug() << "failed!"; + + // Install files outside of cab archives + installDirectories(disk.absolutePath()); + + mTribunalDone = true; + } + + temp.setPath(tempPath); + disk.setPath(diskPath); + + if (mInstallBloodmoon) + { + emit textChanged(QLatin1String("Installing Bloodmoon\n")); + + if (!temp.mkdir(QLatin1String("bloodmoon"))) { + qDebug() << "Can't make dir"; + return; + } + + if (!temp.cd(QLatin1String("bloodmoon"))) { + qDebug() << "Can't cd to dir"; + return; + } + + if (!disk.cd(QLatin1String("Bloodmoon"))) + qDebug() << "Show file selector"; + + if (!disk.exists(QLatin1String("data1.hdr"))) { + qDebug() << "No data found!"; + return; + } + + // Extract the installation files + extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); + + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) + qDebug() << "failed!"; + + // Install files outside of cab archives + installDirectories(disk.absolutePath()); + + mBloodmoonDone = true; + } -// bfs::rename(findFile(mwExtractPath, "morrowind.ini"), outputDataFilesDir / "Morrowind.ini"); + int total = 0; -// mTribunalDone = contains(outputDataFilesDir, "tribunal.esm"); -// mBloodmoonDone = contains(outputDataFilesDir, "bloodmoon.esm"); + if (mInstallMorrowind) + total = 100; -// } + if (mInstallTribunal) + total = total + 100; + if (mInstallBloodmoon) + total = total + 100; - - /// + emit textChanged(tr("Installation finished!")); + emit progressChanged(total); emit finished(); } @@ -322,17 +421,25 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp fileName.append(QString::fromLatin1(unshield_file_name(unshield, index))); // Calculate the percentage done - int progress = qCeil(((float) counter / (float) unshield_file_count(unshield)) * 100); + int progress = qFloor(((float) counter / (float) unshield_file_count(unshield)) * 100); + + if (mMorrowindDone) + progress = progress + 100; + + if (mTribunalDone) + progress = progress + 100; qDebug() << progress << counter << unshield_file_count(unshield); - emit textChanged(QLatin1String("Extracting: ") + QString::fromLatin1(unshield_file_name(unshield, index))); + emit textChanged(tr("Extracting: %1").arg(QString::fromLatin1(unshield_file_name(unshield, index)))); emit progressChanged(progress); success = unshield_file_save(unshield, index, fileName.toLatin1().constData()); - if (!success) + if (!success) { + emit error(tr("Failed to extract %1").arg(fileName)); dir.remove(fileName); + } return success; } diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index 61e021865f..b268727ce9 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -31,6 +31,9 @@ namespace Wizard bool removeDirectory(const QString &dirName); + bool copyFile(const QString &source, const QString &destination, bool keepSource = true); + bool copyDirectory(const QString &source, const QString &destination, bool keepSource = true); + bool moveFile(const QString &source, const QString &destination); bool moveDirectory(const QString &source, const QString &destination); @@ -39,11 +42,17 @@ namespace Wizard void extractCab(const QString &cabFile, const QString &outputDir); bool extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter); + void installDirectories(const QString &source); + bool mInstallMorrowind; bool mInstallTribunal; bool mInstallBloodmoon; + bool mMorrowindDone; + bool mTribunalDone; + bool mBloodmoonDone; + QString mPath; QString mIniPath; @@ -58,6 +67,8 @@ namespace Wizard signals: void finished(); void textChanged(const QString &text); + void logTextChanged(const QString &text); + void error(const QString &text); void progressChanged(int progress); diff --git a/files/ui/wizard/installationpage.ui b/files/ui/wizard/installationpage.ui index 6c1a3c4930..664e105560 100644 --- a/files/ui/wizard/installationpage.ui +++ b/files/ui/wizard/installationpage.ui @@ -6,8 +6,8 @@ 0 0 - 516 - 421 + 514 + 419 @@ -30,7 +30,7 @@ - 24 + 0 From cdbf5c68b0fccceeb9a3a22fc654934228958f89 Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 24 Jan 2014 22:25:22 +0100 Subject: [PATCH 020/303] Implemented selection of installation media and added forgotten file --- apps/wizard/installationpage.cpp | 129 +++- apps/wizard/installationpage.hpp | 8 + apps/wizard/unshield/unshieldworker.cpp | 550 ++++++++++++++---- apps/wizard/unshield/unshieldworker.hpp | 34 ++ .../48x48/preferences-desktop-locale.png | Bin 0 -> 1761 bytes 5 files changed, 568 insertions(+), 153 deletions(-) create mode 100644 files/wizard/icons/tango/48x48/preferences-desktop-locale.png diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 045138ab71..860cfbb568 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include "mainwizard.hpp" #include "inisettings.hpp" @@ -53,82 +55,161 @@ void Wizard::InstallationPage::startInstallation() QStringList components(field("installation.components").toStringList()); QString path(field("installation.path").toString()); - QThread* thread = new QThread(); - UnshieldWorker* unshield = new UnshieldWorker(); + QThread *thread = new QThread(); + mUnshield = new UnshieldWorker(); - unshield->moveToThread(thread); + mUnshield->moveToThread(thread); connect(thread, SIGNAL(started()), - unshield, SLOT(extract())); + mUnshield, SLOT(extract())); - connect(unshield, SIGNAL(finished()), + connect(mUnshield, SIGNAL(finished()), thread, SLOT(quit())); - connect(unshield, SIGNAL(finished()), - unshield, SLOT(deleteLater())); + connect(mUnshield, SIGNAL(finished()), + mUnshield, SLOT(deleteLater())); - connect(unshield, SIGNAL(finished()), + connect(mUnshield, SIGNAL(finished()), thread, SLOT(deleteLater())); - connect(unshield, SIGNAL(finished()), + connect(mUnshield, SIGNAL(finished()), this, SLOT(installationFinished())); - connect(unshield, SIGNAL(error(QString)), + connect(mUnshield, SIGNAL(error(QString)), this, SLOT(installationError(QString))); - connect(unshield, SIGNAL(textChanged(QString)), + connect(mUnshield, SIGNAL(textChanged(QString)), installProgressLabel, SLOT(setText(QString))); - connect(unshield, SIGNAL(textChanged(QString)), + connect(mUnshield, SIGNAL(textChanged(QString)), logTextEdit, SLOT(append(QString))); - connect(unshield, SIGNAL(progressChanged(int)), + connect(mUnshield, SIGNAL(progressChanged(int)), installProgressBar, SLOT(setValue(int))); + connect(mUnshield, SIGNAL(requestFileDialog(QString)), + this, SLOT(showFileDialog(QString))); + if (field("installation.new").toBool() == true) { // Always install Morrowind - unshield->setInstallMorrowind(true); + mUnshield->setInstallMorrowind(true); if (components.contains(QLatin1String("Tribunal"))) - unshield->setInstallTribunal(true); + mUnshield->setInstallTribunal(true); if (components.contains(QLatin1String("Bloodmoon"))) - unshield->setInstallBloodmoon(true); + mUnshield->setInstallBloodmoon(true); } else { // Morrowind should already be installed - unshield->setInstallMorrowind(false); + mUnshield->setInstallMorrowind(false); if (components.contains(QLatin1String("Tribunal")) && mWizard->mInstallations[path]->hasTribunal == false) - unshield->setInstallTribunal(true); + mUnshield->setInstallTribunal(true); if (components.contains(QLatin1String("Bloodmoon")) && mWizard->mInstallations[path]->hasBloodmoon == false) - unshield->setInstallBloodmoon(true); + mUnshield->setInstallBloodmoon(true); // Set the location of the Morrowind.ini to update - unshield->setIniPath(mWizard->mInstallations[path]->iniPath); + mUnshield->setIniPath(mWizard->mInstallations[path]->iniPath); } // Set the installation target path - unshield->setPath(path); + mUnshield->setPath(path); // Set the right codec to use for Morrowind.ini QString language(field("installation.language").toString()); if (language == QLatin1String("Polish")) { - unshield->setIniCodec(QTextCodec::codecForName("windows-1250")); + mUnshield->setIniCodec(QTextCodec::codecForName("windows-1250")); } else if (language == QLatin1String("Russian")) { - unshield->setIniCodec(QTextCodec::codecForName("windows-1251")); + mUnshield->setIniCodec(QTextCodec::codecForName("windows-1251")); } else { - unshield->setIniCodec(QTextCodec::codecForName("windows-1252")); + mUnshield->setIniCodec(QTextCodec::codecForName("windows-1252")); } thread->start(); +} + + +//void Wizard::InstallationPage::installAddons() +//{ +// qDebug() << "component finished"; + +// QStringList components(field("installation.components").toStringList()); + +// if (components.contains(QLatin1String("Tribunal")) && !mUnshield->tribunalDone()) +// { +// QString fileName = QFileDialog::getOpenFileName( +// this, +// tr("Select Tribunal installation file"), +// QDir::rootPath(), +// tr("InstallShield header files (*.hdr)")); + +// if (fileName.isEmpty()) { +// qDebug() << "Cancel was clicked!"; +// return; +// } + +// QFileInfo info(fileName); +// mUnshield->installTribunal(info.absolutePath()); +// } + +// if (components.contains(QLatin1String("Bloodmoon")) && !mUnshield->bloodmoonDone()) +// { +// QString fileName = QFileDialog::getOpenFileName( +// this, +// tr("Select Bloodmoon installation file"), +// QDir::rootPath(), +// tr("InstallShield header files (*.hdr)")); + +// if (fileName.isEmpty()) { +// qDebug() << "Cancel was clicked!"; +// return; +// } + +// QFileInfo info(fileName); +// mUnshield->installBloodmoon(info.absolutePath()); +// } +//} + +void Wizard::InstallationPage::showFileDialog(const QString &component) +{ + QString fileName; + + if (field("installation.new").toBool() == true) + { + fileName = QFileDialog::getOpenFileName( + this, + tr("Select %0 installation file").arg(component), + QDir::rootPath(), + tr("InstallShield header files (*.hdr)")); + + if (fileName.isEmpty()) { + qDebug() << "Cancel was clicked!"; + return; + } + + QFileInfo info(fileName); + + if (component == QLatin1String("Morrowind")) + { + mUnshield->setMorrowindPath(info.absolutePath()); + } + else if (component == QLatin1String("Tribunal")) + { + mUnshield->setTribunalPath(info.absolutePath()); + } + else if (component == QLatin1String("Bloodmoon")) + { + mUnshield->setBloodmoonPath(info.absolutePath()); + } + } } void Wizard::InstallationPage::installationFinished() diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index f55ab8ab5b..e125eadc4a 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -6,10 +6,13 @@ #include "ui_installationpage.h" #include "inisettings.hpp" +class QThread; + namespace Wizard { class MainWizard; class IniSettings; + class UnshieldWorker; class InstallationPage : public QWizardPage, private Ui::InstallationPage { @@ -24,9 +27,14 @@ namespace Wizard MainWizard *mWizard; bool mFinished; + QThread* mThread; + UnshieldWorker *mUnshield; + void startInstallation(); private slots: + void showFileDialog(const QString &component); + void installationFinished(); void installationError(const QString &text); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 4f92b1cf9b..7d536affa3 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -20,6 +21,10 @@ Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : { unshield_set_log_level(0); + mMorrowindPath = QString(); + mTribunalPath = QString(); + mBloodmoonPath = QString(); + mPath = QString(); mIniPath = QString(); @@ -42,19 +47,101 @@ Wizard::UnshieldWorker::~UnshieldWorker() void Wizard::UnshieldWorker::setInstallMorrowind(bool install) { +// QMutexLocker locker(&mMutex); mInstallMorrowind = install; } void Wizard::UnshieldWorker::setInstallTribunal(bool install) { +// QMutexLocker locker(&mMutex); mInstallTribunal = install; } void Wizard::UnshieldWorker::setInstallBloodmoon(bool install) { +// QMutexLocker locker(&mMutex); mInstallBloodmoon = install; } +bool Wizard::UnshieldWorker::getInstallMorrowind() +{ +// QMutexLocker locker(&mMutex); + return mInstallMorrowind; +} + +bool Wizard::UnshieldWorker::getInstallTribunal() +{ +// QMutexLocker locker(&mMutex); + return mInstallTribunal; +} + +bool Wizard::UnshieldWorker::getInstallBloodmoon() +{ +// QMutexLocker locker(&mMutex); + return mInstallBloodmoon; +} + +//bool Wizard::UnshieldWorker::getMorrowindDone() +//{ +// QMutexLocker locker(&mMutex); +// return mMorrowindDone; +//} + +//bool Wizard::UnshieldWorker::tribunalDone() +//{ +// QMutexLocker locker(&mMutex); +// return mTribunalDone; +//} + +//bool Wizard::UnshieldWorker::bloodmoonDone() +//{ +// return mBloodmoonDone; +//} + +void Wizard::UnshieldWorker::setMorrowindPath(const QString &path) +{ + qDebug() << "setmorrowindpath!"; + QMutexLocker locker(&mMutex); + mMorrowindPath = path; + mWait.wakeAll(); +} + +void Wizard::UnshieldWorker::setTribunalPath(const QString &path) +{ + //QMutexLocker locker(&mMutex); + mTribunalPath = path; + mWait.wakeAll(); + +} + +void Wizard::UnshieldWorker::setBloodmoonPath(const QString &path) +{ + //QMutexLocker locker(&mMutex); + mBloodmoonPath = path; + mWait.wakeAll(); + +} + +QString Wizard::UnshieldWorker::getMorrowindPath() +{ + qDebug() << "getmorrowindpath!"; + //QMutexLocker locker(&mMutex); + //mWait.wakeAll(); + return mMorrowindPath; +} + +QString Wizard::UnshieldWorker::getTribunalPath() +{ + //QMutexLocker locker(&mMutex); + return mTribunalPath; +} + +QString Wizard::UnshieldWorker::getBloodmoonPath() +{ + //QMutexLocker locker(&mMutex); + return mBloodmoonPath; +} + void Wizard::UnshieldWorker::setPath(const QString &path) { mPath = path; @@ -224,158 +311,146 @@ void Wizard::UnshieldWorker::installDirectories(const QString &source) copyDirectory(info.absoluteFilePath(), mPath + QDir::separator() + info.fileName()); } } + + // Copy the Data Files dir too, but only the subdirectories + QFileInfo info(dir.absoluteFilePath("Data Files")); + if (info.exists()) { + emit textChanged(tr("Extracting: Data Files directory")); + copyDirectory(info.absoluteFilePath(), mPath); + } + } + void Wizard::UnshieldWorker::extract() { - emit textChanged(QLatin1String("Starting installation")); - emit textChanged(QLatin1String("Installation target: ") + mPath); + qDebug() << "extract!"; - QString diskPath("/mnt/cdrom/"); - QDir disk(diskPath); + QMutexLocker locker(&mMutex); - // Create temporary extract directory - // TODO: Use QTemporaryDir in Qt 5.0 - QString tempPath(mPath + QDir::separator() + QLatin1String("extract-temp")); - QDir temp; + QDir disk; - // Make sure the temporary folder is empty - removeDirectory(tempPath); + qDebug() << "hi!"; - if (!temp.mkpath(tempPath)) { - qDebug() << "Can't make path"; - return; - } - - temp.setPath(tempPath); - disk.setPath(diskPath); - - if (mInstallMorrowind) + if (getInstallMorrowind()) { - emit textChanged(QLatin1String("Installing Morrowind\n")); + while (!mMorrowindDone) + { + if (getMorrowindPath().isEmpty()) { + qDebug() << "request file dialog"; + emit requestFileDialog(QLatin1String("Morrowind")); + mWait.wait(&mMutex); + } - if (!temp.mkdir(QLatin1String("morrowind"))) { - qDebug() << "Can't make dir"; - return; + if (!mMorrowindDone && !getMorrowindPath().isEmpty()) { + disk.setPath(getMorrowindPath()); + + if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Morrowind.bsa"))) { + qDebug() << "found"; + emit requestFileDialog(QLatin1String("Morrowind")); + mWait.wait(&mMutex); + + } else { + qDebug() << "install morrowind!"; + + if (installMorrowind()) { + mMorrowindDone = true; + } else { + qDebug() << "Erorr installing Morrowind"; + return; + } + } + } } - - if (!temp.cd(QLatin1String("morrowind"))) { - qDebug() << "Can't cd to dir"; - return; - } - - if (!disk.exists(QLatin1String("data1.hdr"))) { - qDebug() << "No data found!"; - return; - } - - // Extract the installation files - extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); - - // TODO: Throw error; - // Move the files from the temporary path to the destination folder - - //qDebug() << "rename: " << morrowindTempPath << " to: " << mPath; - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) - qDebug() << "failed!"; - - // Install files outside of cab archives - qDebug() << temp.absolutePath() << disk.absolutePath(); - installDirectories(disk.absolutePath()); - - // Copy Morrowind configuration file - QString iniPath(temp.absoluteFilePath(QLatin1String("App Executables"))); - iniPath.append(QDir::separator() + QLatin1String("Morrowind.ini")); - - QFileInfo info(iniPath); - - qDebug() << info.absoluteFilePath() << mPath; - - if (info.exists()) { - emit textChanged(tr("Extracting: Morrowind.ini")); - moveFile(info.absoluteFilePath(), mPath + QDir::separator() + QLatin1String("Morrowind.ini")); - } else { - qDebug() << "Could not find ini file!"; - } - - mMorrowindDone = true; - } - temp.setPath(tempPath); - disk.setPath(diskPath); - - if (mInstallTribunal) + if (getInstallTribunal()) { - emit textChanged(QLatin1String("Installing Tribunal\n")); + while (!mTribunalDone) + { + QDir tribunal(disk); - if (!temp.mkdir(QLatin1String("tribunal"))) { - qDebug() << "Can't make dir"; - return; + if (!tribunal.cd(QLatin1String("Tribunal"))) { + qDebug() << "not found on cd!"; + emit requestFileDialog(QLatin1String("Tribunal")); + mWait.wait(&mMutex); + + } else if (tribunal.exists(QLatin1String("data1.hdr"))) { + qDebug() << "Exists! " << tribunal.absolutePath(); + mTribunalPath = tribunal.absolutePath(); + } + + if (getTribunalPath().isEmpty()) { + qDebug() << "request file dialog"; + emit requestFileDialog(QLatin1String("Tribunal")); + mWait.wait(&mMutex); + } + + // Make sure the dir is up-to-date + tribunal.setPath(getTribunalPath()); + + if (!findFile(tribunal.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa"))) { + qDebug() << "file not found!"; + emit requestFileDialog(QLatin1String("Tribunal")); + mWait.wait(&mMutex); + + } else { + qDebug() << "install tribunal!"; + if (installTribunal()) { + mTribunalDone = true; + } else { + qDebug() << "Erorr installing Tribunal"; + return; + } + } } - - if (!temp.cd(QLatin1String("tribunal"))) { - qDebug() << "Can't cd to dir"; - return; - } - - if (!disk.cd(QLatin1String("Tribunal"))) - qDebug() << "Show file selector"; - - if (!disk.exists(QLatin1String("data1.hdr"))) { - qDebug() << "No data found!"; - return; - } - - // Extract the installation files - extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); - - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) - qDebug() << "failed!"; - - // Install files outside of cab archives - installDirectories(disk.absolutePath()); - - mTribunalDone = true; } - temp.setPath(tempPath); - disk.setPath(diskPath); - - if (mInstallBloodmoon) + if (getInstallBloodmoon()) { - emit textChanged(QLatin1String("Installing Bloodmoon\n")); + while (!mBloodmoonDone) + { + QDir bloodmoon(disk); - if (!temp.mkdir(QLatin1String("bloodmoon"))) { - qDebug() << "Can't make dir"; - return; + qDebug() << "bloodmoon!: " << bloodmoon.absolutePath(); + + if (!bloodmoon.cd(QLatin1String("Bloodmoon"))) { + emit requestFileDialog(QLatin1String("Bloodmoon")); + mWait.wait(&mMutex); + + } else if (bloodmoon.exists(QLatin1String("data1.hdr"))) { + mBloodmoonPath = bloodmoon.absolutePath(); + } + + if (getBloodmoonPath().isEmpty()) { + qDebug() << "request file dialog"; + emit requestFileDialog(QLatin1String("Bloodmoon")); + mWait.wait(&mMutex); + } + + // Make sure the dir is up-to-date + bloodmoon.setPath(getBloodmoonPath()); + + if (!findFile(bloodmoon.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) { + emit requestFileDialog(QLatin1String("Bloodmoon")); + mWait.wait(&mMutex); + + } else { + qDebug() << "install bloodmoon!"; + if (installBloodmoon()) { + mBloodmoonDone = true; + } else { + qDebug() << "Erorr installing Bloodmoon"; + return; + } + } } - - if (!temp.cd(QLatin1String("bloodmoon"))) { - qDebug() << "Can't cd to dir"; - return; - } - - if (!disk.cd(QLatin1String("Bloodmoon"))) - qDebug() << "Show file selector"; - - if (!disk.exists(QLatin1String("data1.hdr"))) { - qDebug() << "No data found!"; - return; - } - - // Extract the installation files - extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); - - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) - qDebug() << "failed!"; - - // Install files outside of cab archives - installDirectories(disk.absolutePath()); - - mBloodmoonDone = true; } + // Remove the temporary directory + removeDirectory(mPath + QDir::separator() + QLatin1String("extract-temp")); + + locker.unlock(); int total = 0; @@ -392,8 +467,190 @@ void Wizard::UnshieldWorker::extract() emit progressChanged(total); emit finished(); + qDebug() << "installation finished!"; } +bool Wizard::UnshieldWorker::installMorrowind() +{ + emit textChanged(QLatin1String("Installing Morrowind\n")); + + QDir disk(getMorrowindPath()); + + if (!disk.exists()) + return false; + + // Create temporary extract directory + // TODO: Use QTemporaryDir in Qt 5.0 + QString tempPath(mPath + QDir::separator() + QLatin1String("extract-temp")); + QDir temp; + + // Make sure the temporary folder is empty + removeDirectory(tempPath); + + if (!temp.mkpath(tempPath)) { + qDebug() << "Can't make path"; + return false; + } + + temp.setPath(tempPath); + + QString cabFile(disk.absoluteFilePath(QLatin1String("data1.hdr"))); + + if (!temp.mkdir(QLatin1String("morrowind"))) { + qDebug() << "Can't make dir"; + return false; + } + + if (!temp.cd(QLatin1String("morrowind"))) { + qDebug() << "Can't cd to dir"; + return false; + } + + // Extract the installation files + extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); + + // TODO: Throw error; + // Move the files from the temporary path to the destination folder + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) { + qDebug() << "failed to move files!"; + return false; + } + + // Install files outside of cab archives + installDirectories(disk.absolutePath()); + + // Copy Morrowind configuration file + QString iniPath(temp.absoluteFilePath(QLatin1String("App Executables"))); + iniPath.append(QDir::separator() + QLatin1String("Morrowind.ini")); + + QFileInfo info(iniPath); + + qDebug() << info.absoluteFilePath() << mPath; + + if (info.exists()) { + emit textChanged(tr("Extracting: Morrowind.ini")); + moveFile(info.absoluteFilePath(), mPath + QDir::separator() + QLatin1String("Morrowind.ini")); + } else { + qDebug() << "Could not find ini file!"; + return false; + } + + return true; +} + +bool Wizard::UnshieldWorker::installTribunal() +{ + emit textChanged(QLatin1String("Installing Tribunal")); + + QDir disk(getTribunalPath()); + + if (!disk.exists()) { + qDebug() << "disk does not exist! " << disk.absolutePath() << getTribunalPath(); + return false; + } + + // Create temporary extract directory + // TODO: Use QTemporaryDir in Qt 5.0 + QString tempPath(mPath + QDir::separator() + QLatin1String("extract-temp")); + QDir temp; + + // Make sure the temporary folder is empty + removeDirectory(tempPath); + + if (!temp.mkpath(tempPath)) { + qDebug() << "Can't make path"; + return false; + } + + temp.setPath(tempPath); + + if (!temp.mkdir(QLatin1String("tribunal"))) { + qDebug() << "Can't make dir"; + return false; + } + + if (!temp.cd(QLatin1String("tribunal"))) { + qDebug() << "Can't cd to dir"; + return false; + } + + // Extract the installation files + extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); + + // TODO: Throw error; + // Move the files from the temporary path to the destination folder + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) { + qDebug() << "failed to move files!"; + return false; + } + + // Install files outside of cab archives + installDirectories(disk.absolutePath()); + + return true; +} + +bool Wizard::UnshieldWorker::installBloodmoon() +{ + emit textChanged(QLatin1String("Installing Bloodmoon")); + + QDir disk(getBloodmoonPath()); + + if (!disk.exists()) { + qDebug() << "disk does not exist! " << disk.absolutePath() << getTribunalPath(); + return false; + } + + // Create temporary extract directory + // TODO: Use QTemporaryDir in Qt 5.0 + QString tempPath(mPath + QDir::separator() + QLatin1String("extract-temp")); + QDir temp; + + // Make sure the temporary folder is empty + removeDirectory(tempPath); + + if (!temp.mkpath(tempPath)) { + qDebug() << "Can't make path"; + return false; + } + + temp.setPath(tempPath); + + if (!temp.mkdir(QLatin1String("bloodmoon"))) { + qDebug() << "Can't make dir"; + return false; + } + + if (!temp.cd(QLatin1String("bloodmoon"))) { + qDebug() << "Can't cd to dir"; + return false; + } + + // Extract the installation files + extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); + + // TODO: Throw error; + // Move the files from the temporary path to the destination folder + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) { + qDebug() << "failed to move files!"; + return false; + } + + // Install files outside of cab archives + installDirectories(disk.absolutePath()); + + QFileInfo patch(temp.absoluteFilePath(QLatin1String("Tribunal Patch") + QDir::separator() + QLatin1String("Tribunal.esm"))); + QFileInfo original(mPath + QDir::separator() + QLatin1String("Tribunal.esm")); + + if (original.exists() && patch.exists()) { + emit textChanged(tr("Extracting: Tribunal patch")); + copyFile(patch.absoluteFilePath(), original.absoluteFilePath()); + } + + return true; +} + + bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter) { bool success; @@ -444,11 +701,46 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp return success; } +bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fileName) +{ + Unshield *unshield; + unshield = unshield_open(cabFile.toLatin1().constData()); + + // TODO: Proper error + if (!unshield) { + emit error(tr("Failed to open %1").arg(cabFile)); + return false; + } + + for (int i=0; ifirst_file; j<=group->last_file; ++j) + { + QString current(QString::fromLatin1(unshield_file_name(unshield, j))); + + qDebug() << "File is: " << unshield_file_name(unshield, j); + if (current == fileName) + return true; // File is found! + } + } + + unshield_close(unshield); + return false; +} + void Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &outputDir) { Unshield *unshield; unshield = unshield_open(cabFile.toLatin1().constData()); + // TODO: Proper error + if (!unshield) { + emit error(tr("Failed to open %1").arg(cabFile)); + return; + } + int counter = 0; for (int i=0; i #include +#include +#include #include @@ -22,6 +24,18 @@ namespace Wizard void setInstallTribunal(bool install); void setInstallBloodmoon(bool install); + bool getInstallMorrowind(); + bool getInstallTribunal(); + bool getInstallBloodmoon(); + + void setMorrowindPath(const QString &path); + void setTribunalPath(const QString &path); + void setBloodmoonPath(const QString &path); + + QString getMorrowindPath(); + QString getTribunalPath(); + QString getBloodmoonPath(); + void setPath(const QString &path); void setIniPath(const QString &path); @@ -29,6 +43,14 @@ namespace Wizard private: +// void setMorrowindDone(bool done); +// void setTribunalDone(bool done); +// void setBloodmoonDone(bool done); + +// bool getMorrowindDone(); +// bool getTribunalDone(); +// bool getBloodmoonDone(); + bool removeDirectory(const QString &dirName); bool copyFile(const QString &source, const QString &destination, bool keepSource = true); @@ -41,9 +63,13 @@ namespace Wizard void extractCab(const QString &cabFile, const QString &outputDir); bool extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter); + bool findFile(const QString &cabFile, const QString &fileName); void installDirectories(const QString &source); + bool installMorrowind(); + bool installTribunal(); + bool installBloodmoon(); bool mInstallMorrowind; bool mInstallTribunal; @@ -53,6 +79,10 @@ namespace Wizard bool mTribunalDone; bool mBloodmoonDone; + QString mMorrowindPath; + QString mTribunalPath; + QString mBloodmoonPath; + QString mPath; QString mIniPath; @@ -60,12 +90,16 @@ namespace Wizard QTextCodec *mIniCodec; + QWaitCondition mWait; + QMutex mMutex; public slots: void extract(); signals: void finished(); + void requestFileDialog(const QString &component); + void textChanged(const QString &text); void logTextChanged(const QString &text); diff --git a/files/wizard/icons/tango/48x48/preferences-desktop-locale.png b/files/wizard/icons/tango/48x48/preferences-desktop-locale.png new file mode 100644 index 0000000000000000000000000000000000000000..f56497bd23989040f0a2d23080b8103ad086cfbf GIT binary patch literal 1761 zcmV<71|Io|P)f8XQYxp&4h_IP4DiIX;ILTG3tgaC<(1skfaDuhs3pe_p%ELpSg zo((HNVgnl%tyF=ET2vukf$$J%)uuF+lR9pi_-)U_Gk5Oed$8~jEaW&-M~1PKBVFB@ zJ6D?X`@VDj=bR57*&}->6Z`%6!sovINg>SBGSFX?LOm|v1W?9%oO9L-(FVU^jd|C@ zycGTeTlfzyymk7cIp*B|1>pIoo)6{gZIO`BjAP`8Q$chZ>4Axt`a)fa}RE?qJr6vv%WPvw2}>zDue!|r|n zdiNJ4)Xy({_Q|QK6H|0HIxMU&5(Xhb5D)@T^e?8{>BTh3zgA>6H<_{48s#u0mG7*mkfBA|CDDsB3&Jow(6;uv3F&4^mZvDGP*M|UO%%n%QA}707#ojbyo_-kZ+3z~58l;EvGC#T zt}Pz?U;2Ng7T$aEEXNg&G*M`&?>VcaMyZ3jB9FR^%DppN9BqIjzXL}b!22TwpzmAW z_TlitvK8N8y*5vK>?T>MNh&*dX~BbV8VZffG`?BGY>eZ3Rp?EidUZe@GQfSTueNx5 z;#EG3|H5qJERWAV!R*wtjK>YeN|Qu-44kB}1*XX9q+6_RFS6dc!|LV=&GsT&=^C<7 z1f5C3%^9NYX`=2lQ95-Xu%Q8N)?eqwKbkWdQXc86EXzPV7|=qDn-0EFRerNADFM)%qCYljDpxYIqN6FU8?U9>D&a zLIsK&?=6@+x9%2cKMTEZEB-})auj7~H-G_n^UrUmX+JGGY3H@u8w1qg+fp7qz9z3Is2*A~) zs|h=!i5mc@!8>9nKitSTe*G0+As=Yf%%8@xjT@?(XD_x@fwNTDdJIu$iZMNbf_F|-J<(vhh? znM~-*Z4%L;ZB{8L4qt`#v3v3}89qiMJWWM6=x1HJ?S$>TiM1O@uZ%zwm#VT7HstZ} zJeBe|VJ)JauhC5JvejRr*lpFaXY5`fbmZyMIhqR+fafwDW{p z`L4hvwPFt$LxtH?f7Um$md(V@%!t{&}?jg Date: Mon, 27 Jan 2014 15:12:02 +0100 Subject: [PATCH 021/303] Made the getter/setter functions thread-safe --- apps/wizard/installationpage.cpp | 12 +- apps/wizard/unshield/unshieldworker.cpp | 273 ++++++++++++------------ apps/wizard/unshield/unshieldworker.hpp | 18 +- 3 files changed, 155 insertions(+), 148 deletions(-) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 860cfbb568..9b14b5918f 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -73,22 +73,22 @@ void Wizard::InstallationPage::startInstallation() thread, SLOT(deleteLater())); connect(mUnshield, SIGNAL(finished()), - this, SLOT(installationFinished())); + this, SLOT(installationFinished()), Qt::QueuedConnection); connect(mUnshield, SIGNAL(error(QString)), - this, SLOT(installationError(QString))); + this, SLOT(installationError(QString)), Qt::QueuedConnection); connect(mUnshield, SIGNAL(textChanged(QString)), - installProgressLabel, SLOT(setText(QString))); + installProgressLabel, SLOT(setText(QString)), Qt::QueuedConnection); connect(mUnshield, SIGNAL(textChanged(QString)), - logTextEdit, SLOT(append(QString))); + logTextEdit, SLOT(append(QString)), Qt::QueuedConnection); connect(mUnshield, SIGNAL(progressChanged(int)), - installProgressBar, SLOT(setValue(int))); + installProgressBar, SLOT(setValue(int)), Qt::QueuedConnection); connect(mUnshield, SIGNAL(requestFileDialog(QString)), - this, SLOT(showFileDialog(QString))); + this, SLOT(showFileDialog(QString)), Qt::QueuedConnection); if (field("installation.new").toBool() == true) { diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 7d536affa3..5c22df4464 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -2,10 +2,11 @@ #include +#include +#include #include #include #include -#include #include #include #include @@ -47,68 +48,50 @@ Wizard::UnshieldWorker::~UnshieldWorker() void Wizard::UnshieldWorker::setInstallMorrowind(bool install) { -// QMutexLocker locker(&mMutex); + QWriteLocker writeLock(&mLock); mInstallMorrowind = install; } void Wizard::UnshieldWorker::setInstallTribunal(bool install) { -// QMutexLocker locker(&mMutex); + QWriteLocker writeLock(&mLock); mInstallTribunal = install; } void Wizard::UnshieldWorker::setInstallBloodmoon(bool install) { -// QMutexLocker locker(&mMutex); + QWriteLocker writeLock(&mLock); mInstallBloodmoon = install; } bool Wizard::UnshieldWorker::getInstallMorrowind() { -// QMutexLocker locker(&mMutex); + QReadLocker readLock(&mLock); return mInstallMorrowind; } bool Wizard::UnshieldWorker::getInstallTribunal() { -// QMutexLocker locker(&mMutex); + QReadLocker readLock(&mLock); return mInstallTribunal; } bool Wizard::UnshieldWorker::getInstallBloodmoon() { -// QMutexLocker locker(&mMutex); + QReadLocker readLock(&mLock); return mInstallBloodmoon; } -//bool Wizard::UnshieldWorker::getMorrowindDone() -//{ -// QMutexLocker locker(&mMutex); -// return mMorrowindDone; -//} - -//bool Wizard::UnshieldWorker::tribunalDone() -//{ -// QMutexLocker locker(&mMutex); -// return mTribunalDone; -//} - -//bool Wizard::UnshieldWorker::bloodmoonDone() -//{ -// return mBloodmoonDone; -//} - void Wizard::UnshieldWorker::setMorrowindPath(const QString &path) { - qDebug() << "setmorrowindpath!"; - QMutexLocker locker(&mMutex); + QWriteLocker writeLock(&mLock); mMorrowindPath = path; mWait.wakeAll(); } void Wizard::UnshieldWorker::setTribunalPath(const QString &path) { - //QMutexLocker locker(&mMutex); + QWriteLocker writeLock(&mLock); mTribunalPath = path; mWait.wakeAll(); @@ -116,7 +99,7 @@ void Wizard::UnshieldWorker::setTribunalPath(const QString &path) void Wizard::UnshieldWorker::setBloodmoonPath(const QString &path) { - //QMutexLocker locker(&mMutex); + QWriteLocker writeLock(&mLock); mBloodmoonPath = path; mWait.wakeAll(); @@ -124,57 +107,99 @@ void Wizard::UnshieldWorker::setBloodmoonPath(const QString &path) QString Wizard::UnshieldWorker::getMorrowindPath() { - qDebug() << "getmorrowindpath!"; - //QMutexLocker locker(&mMutex); - //mWait.wakeAll(); + QReadLocker readLock(&mLock); return mMorrowindPath; } QString Wizard::UnshieldWorker::getTribunalPath() { - //QMutexLocker locker(&mMutex); + QReadLocker readLock(&mLock); return mTribunalPath; } QString Wizard::UnshieldWorker::getBloodmoonPath() { - //QMutexLocker locker(&mMutex); + QReadLocker readLock(&mLock); return mBloodmoonPath; } void Wizard::UnshieldWorker::setPath(const QString &path) { + QWriteLocker writeLock(&mLock); mPath = path; } void Wizard::UnshieldWorker::setIniPath(const QString &path) { + QWriteLocker writeLock(&mLock); mIniPath = path; } +QString Wizard::UnshieldWorker::getPath() +{ + QReadLocker readLock(&mLock); + return mPath; +} + +QString Wizard::UnshieldWorker::getIniPath() +{ + QReadLocker readLock(&mLock); + return mIniPath; +} + void Wizard::UnshieldWorker::setIniCodec(QTextCodec *codec) { + QWriteLocker writeLock(&mLock); mIniCodec = codec; } +void Wizard::UnshieldWorker::setMorrowindDone(bool done) +{ + QWriteLocker writeLock(&mLock); + mMorrowindDone = done; +} + +void Wizard::UnshieldWorker::setTribunalDone(bool done) +{ + QWriteLocker writeLock(&mLock); + mTribunalDone = done; +} + +void Wizard::UnshieldWorker::setBloodmoonDone(bool done) +{ + QWriteLocker writeLock(&mLock); + mBloodmoonDone = done; +} + +bool Wizard::UnshieldWorker::getMorrowindDone() +{ + QReadLocker readLock(&mLock); + return mMorrowindDone; +} + +bool Wizard::UnshieldWorker::getTribunalDone() +{ + QReadLocker readLock(&mLock); + return mTribunalDone; +} + +bool Wizard::UnshieldWorker::getBloodmoonDone() +{ + QReadLocker readLock(&mLock); + return mBloodmoonDone; +} + void Wizard::UnshieldWorker::setupSettings() { // Create Morrowind.ini settings map - if (mIniPath.isEmpty()) + if (getIniPath().isEmpty()) return; - QFile file(mIniPath); + QFile file(getIniPath()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { // TODO: Emit error signal - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error opening Morrowind configuration file")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); - msgBox.exec(); + qDebug() << "Error opening .ini file!"; return; } @@ -308,7 +333,7 @@ void Wizard::UnshieldWorker::installDirectories(const QString &source) if (directories.contains(info.fileName())) { qDebug() << "found " << info.fileName(); emit textChanged(tr("Extracting: %1 directory").arg(info.fileName())); - copyDirectory(info.absoluteFilePath(), mPath + QDir::separator() + info.fileName()); + copyDirectory(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName()); } } @@ -316,7 +341,7 @@ void Wizard::UnshieldWorker::installDirectories(const QString &source) QFileInfo info(dir.absoluteFilePath("Data Files")); if (info.exists()) { emit textChanged(tr("Extracting: Data Files directory")); - copyDirectory(info.absoluteFilePath(), mPath); + copyDirectory(info.absoluteFilePath(), getPath()); } } @@ -325,36 +350,32 @@ void Wizard::UnshieldWorker::installDirectories(const QString &source) void Wizard::UnshieldWorker::extract() { qDebug() << "extract!"; - - QMutexLocker locker(&mMutex); - QDir disk; - qDebug() << "hi!"; - if (getInstallMorrowind()) { - while (!mMorrowindDone) + while (!getMorrowindDone()) { if (getMorrowindPath().isEmpty()) { qDebug() << "request file dialog"; + QReadLocker readLock(&mLock); emit requestFileDialog(QLatin1String("Morrowind")); - mWait.wait(&mMutex); + mWait.wait(&mLock); } - if (!mMorrowindDone && !getMorrowindPath().isEmpty()) { + if (!getMorrowindPath().isEmpty()) { disk.setPath(getMorrowindPath()); - if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Morrowind.bsa"))) { - qDebug() << "found"; + if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Morrowind.bsa")) + | findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa")) + | findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) + { + QReadLocker readLock(&mLock); emit requestFileDialog(QLatin1String("Morrowind")); - mWait.wait(&mMutex); - + mWait.wait(&mLock); } else { - qDebug() << "install morrowind!"; - if (installMorrowind()) { - mMorrowindDone = true; + setMorrowindDone(true); } else { qDebug() << "Erorr installing Morrowind"; return; @@ -366,41 +387,31 @@ void Wizard::UnshieldWorker::extract() if (getInstallTribunal()) { - while (!mTribunalDone) + while (!getTribunalDone()) { - QDir tribunal(disk); - - if (!tribunal.cd(QLatin1String("Tribunal"))) { - qDebug() << "not found on cd!"; - emit requestFileDialog(QLatin1String("Tribunal")); - mWait.wait(&mMutex); - - } else if (tribunal.exists(QLatin1String("data1.hdr"))) { - qDebug() << "Exists! " << tribunal.absolutePath(); - mTribunalPath = tribunal.absolutePath(); - } - if (getTribunalPath().isEmpty()) { qDebug() << "request file dialog"; + QReadLocker locker(&mLock); emit requestFileDialog(QLatin1String("Tribunal")); - mWait.wait(&mMutex); + mWait.wait(&mLock); } - // Make sure the dir is up-to-date - tribunal.setPath(getTribunalPath()); + if (!getTribunalPath().isEmpty()) { + disk.setPath(getTribunalPath()); - if (!findFile(tribunal.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa"))) { - qDebug() << "file not found!"; - emit requestFileDialog(QLatin1String("Tribunal")); - mWait.wait(&mMutex); - - } else { - qDebug() << "install tribunal!"; - if (installTribunal()) { - mTribunalDone = true; + if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa"))) + { + qDebug() << "found"; + QReadLocker locker(&mLock); + emit requestFileDialog(QLatin1String("Tribunal")); + mWait.wait(&mLock); } else { - qDebug() << "Erorr installing Tribunal"; - return; + if (installTribunal()) { + setTribunalDone(true); + } else { + qDebug() << "Erorr installing Tribunal"; + return; + } } } } @@ -408,50 +419,35 @@ void Wizard::UnshieldWorker::extract() if (getInstallBloodmoon()) { - while (!mBloodmoonDone) + while (!getBloodmoonDone()) { - QDir bloodmoon(disk); - - qDebug() << "bloodmoon!: " << bloodmoon.absolutePath(); - - if (!bloodmoon.cd(QLatin1String("Bloodmoon"))) { - emit requestFileDialog(QLatin1String("Bloodmoon")); - mWait.wait(&mMutex); - - } else if (bloodmoon.exists(QLatin1String("data1.hdr"))) { - mBloodmoonPath = bloodmoon.absolutePath(); - } - if (getBloodmoonPath().isEmpty()) { qDebug() << "request file dialog"; + QReadLocker locker(&mLock); emit requestFileDialog(QLatin1String("Bloodmoon")); - mWait.wait(&mMutex); + mWait.wait(&mLock); } - // Make sure the dir is up-to-date - bloodmoon.setPath(getBloodmoonPath()); + if (!getBloodmoonPath().isEmpty()) { + disk.setPath(getBloodmoonPath()); - if (!findFile(bloodmoon.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) { - emit requestFileDialog(QLatin1String("Bloodmoon")); - mWait.wait(&mMutex); - - } else { - qDebug() << "install bloodmoon!"; - if (installBloodmoon()) { - mBloodmoonDone = true; + if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) + { + QReadLocker locker(&mLock); + emit requestFileDialog(QLatin1String("Bloodmoon")); + mWait.wait(&mLock); } else { - qDebug() << "Erorr installing Bloodmoon"; - return; + if (installBloodmoon()) { + setBloodmoonDone(true); + } else { + qDebug() << "Erorr installing Bloodmoon"; + return; + } } } } } - // Remove the temporary directory - removeDirectory(mPath + QDir::separator() + QLatin1String("extract-temp")); - - locker.unlock(); - int total = 0; if (mInstallMorrowind) @@ -472,16 +468,19 @@ void Wizard::UnshieldWorker::extract() bool Wizard::UnshieldWorker::installMorrowind() { - emit textChanged(QLatin1String("Installing Morrowind\n")); + qDebug() << "install morrowind!"; + emit textChanged(QLatin1String("Installing Morrowind")); QDir disk(getMorrowindPath()); - if (!disk.exists()) + if (!disk.exists()) { + qDebug() << "getMorrowindPath: " << getMorrowindPath(); return false; + } // Create temporary extract directory // TODO: Use QTemporaryDir in Qt 5.0 - QString tempPath(mPath + QDir::separator() + QLatin1String("extract-temp")); + QString tempPath(getPath() + QDir::separator() + QLatin1String("extract-temp")); QDir temp; // Make sure the temporary folder is empty @@ -494,8 +493,6 @@ bool Wizard::UnshieldWorker::installMorrowind() temp.setPath(tempPath); - QString cabFile(disk.absoluteFilePath(QLatin1String("data1.hdr"))); - if (!temp.mkdir(QLatin1String("morrowind"))) { qDebug() << "Can't make dir"; return false; @@ -511,7 +508,8 @@ bool Wizard::UnshieldWorker::installMorrowind() // TODO: Throw error; // Move the files from the temporary path to the destination folder - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) { + emit textChanged(tr("Moving installation files")); + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { qDebug() << "failed to move files!"; return false; } @@ -525,16 +523,17 @@ bool Wizard::UnshieldWorker::installMorrowind() QFileInfo info(iniPath); - qDebug() << info.absoluteFilePath() << mPath; + qDebug() << info.absoluteFilePath() << getPath(); if (info.exists()) { emit textChanged(tr("Extracting: Morrowind.ini")); - moveFile(info.absoluteFilePath(), mPath + QDir::separator() + QLatin1String("Morrowind.ini")); + moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); } else { qDebug() << "Could not find ini file!"; return false; } + emit textChanged(tr("Morrowind installation finished!")); return true; } @@ -551,7 +550,7 @@ bool Wizard::UnshieldWorker::installTribunal() // Create temporary extract directory // TODO: Use QTemporaryDir in Qt 5.0 - QString tempPath(mPath + QDir::separator() + QLatin1String("extract-temp")); + QString tempPath(getPath() + QDir::separator() + QLatin1String("extract-temp")); QDir temp; // Make sure the temporary folder is empty @@ -579,7 +578,8 @@ bool Wizard::UnshieldWorker::installTribunal() // TODO: Throw error; // Move the files from the temporary path to the destination folder - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) { + emit textChanged(tr("Moving installation files")); + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { qDebug() << "failed to move files!"; return false; } @@ -587,6 +587,7 @@ bool Wizard::UnshieldWorker::installTribunal() // Install files outside of cab archives installDirectories(disk.absolutePath()); + emit textChanged(tr("Tribunal installation finished!")); return true; } @@ -597,13 +598,12 @@ bool Wizard::UnshieldWorker::installBloodmoon() QDir disk(getBloodmoonPath()); if (!disk.exists()) { - qDebug() << "disk does not exist! " << disk.absolutePath() << getTribunalPath(); return false; } // Create temporary extract directory // TODO: Use QTemporaryDir in Qt 5.0 - QString tempPath(mPath + QDir::separator() + QLatin1String("extract-temp")); + QString tempPath(getPath() + QDir::separator() + QLatin1String("extract-temp")); QDir temp; // Make sure the temporary folder is empty @@ -631,7 +631,8 @@ bool Wizard::UnshieldWorker::installBloodmoon() // TODO: Throw error; // Move the files from the temporary path to the destination folder - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), mPath)) { + emit textChanged(tr("Moving installation files")); + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { qDebug() << "failed to move files!"; return false; } @@ -640,13 +641,14 @@ bool Wizard::UnshieldWorker::installBloodmoon() installDirectories(disk.absolutePath()); QFileInfo patch(temp.absoluteFilePath(QLatin1String("Tribunal Patch") + QDir::separator() + QLatin1String("Tribunal.esm"))); - QFileInfo original(mPath + QDir::separator() + QLatin1String("Tribunal.esm")); + QFileInfo original(getPath() + QDir::separator() + QLatin1String("Tribunal.esm")); if (original.exists() && patch.exists()) { emit textChanged(tr("Extracting: Tribunal patch")); copyFile(patch.absoluteFilePath(), original.absoluteFilePath()); } + emit textChanged(tr("Bloodmoon installation finished!")); return true; } @@ -669,7 +671,6 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp path.replace(QLatin1Char('\\'), QDir::separator()); path = QDir::toNativeSeparators(path); - qDebug() << "path is: " << path << QString::fromLatin1(unshield_directory_name(unshield, directory)) + QLatin1Char('/'); // Ensure the target path exists QDir dir; dir.mkpath(path); @@ -678,12 +679,12 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp fileName.append(QString::fromLatin1(unshield_file_name(unshield, index))); // Calculate the percentage done - int progress = qFloor(((float) counter / (float) unshield_file_count(unshield)) * 100); + int progress = (((float) counter / (float) unshield_file_count(unshield)) * 100); - if (mMorrowindDone) + if (getMorrowindDone()) progress = progress + 100; - if (mTribunalDone) + if (getTribunalDone()) progress = progress + 100; qDebug() << progress << counter << unshield_file_count(unshield); diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index 17c0973b0d..bdaf44198f 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -39,17 +40,20 @@ namespace Wizard void setPath(const QString &path); void setIniPath(const QString &path); + QString getPath(); + QString getIniPath(); + void setIniCodec(QTextCodec *codec); private: -// void setMorrowindDone(bool done); -// void setTribunalDone(bool done); -// void setBloodmoonDone(bool done); + void setMorrowindDone(bool done); + void setTribunalDone(bool done); + void setBloodmoonDone(bool done); -// bool getMorrowindDone(); -// bool getTribunalDone(); -// bool getBloodmoonDone(); + bool getMorrowindDone(); + bool getTribunalDone(); + bool getBloodmoonDone(); bool removeDirectory(const QString &dirName); @@ -93,6 +97,8 @@ namespace Wizard QWaitCondition mWait; QMutex mMutex; + QReadWriteLock mLock; + public slots: void extract(); From 3fd88aca594040a756ba811d6ac06fc4da8de523 Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 27 Jan 2014 16:51:22 +0100 Subject: [PATCH 022/303] Re-added support for GoTY disks and added a messagebox, displayed when done --- apps/wizard/installationpage.cpp | 53 ++++------------ apps/wizard/unshield/unshieldworker.cpp | 80 +++++++++++++++++-------- 2 files changed, 65 insertions(+), 68 deletions(-) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 9b14b5918f..8690cd2bc7 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "mainwizard.hpp" #include "inisettings.hpp" @@ -135,49 +136,6 @@ void Wizard::InstallationPage::startInstallation() thread->start(); } - - -//void Wizard::InstallationPage::installAddons() -//{ -// qDebug() << "component finished"; - -// QStringList components(field("installation.components").toStringList()); - -// if (components.contains(QLatin1String("Tribunal")) && !mUnshield->tribunalDone()) -// { -// QString fileName = QFileDialog::getOpenFileName( -// this, -// tr("Select Tribunal installation file"), -// QDir::rootPath(), -// tr("InstallShield header files (*.hdr)")); - -// if (fileName.isEmpty()) { -// qDebug() << "Cancel was clicked!"; -// return; -// } - -// QFileInfo info(fileName); -// mUnshield->installTribunal(info.absolutePath()); -// } - -// if (components.contains(QLatin1String("Bloodmoon")) && !mUnshield->bloodmoonDone()) -// { -// QString fileName = QFileDialog::getOpenFileName( -// this, -// tr("Select Bloodmoon installation file"), -// QDir::rootPath(), -// tr("InstallShield header files (*.hdr)")); - -// if (fileName.isEmpty()) { -// qDebug() << "Cancel was clicked!"; -// return; -// } - -// QFileInfo info(fileName); -// mUnshield->installBloodmoon(info.absolutePath()); -// } -//} - void Wizard::InstallationPage::showFileDialog(const QString &component) { QString fileName; @@ -215,6 +173,15 @@ void Wizard::InstallationPage::showFileDialog(const QString &component) void Wizard::InstallationPage::installationFinished() { qDebug() << "finished!"; + + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Installation finished")); + msgBox.setIcon(QMessageBox::Information); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("Installation completed sucessfully!")); + + msgBox.exec(); + mFinished = true; emit completeChanged(); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 5c22df4464..2ab68dde09 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -287,7 +287,7 @@ bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString if (info.isDir()) { result = moveDirectory(info.absoluteFilePath(), destDir.absolutePath() + relativePath); } else { - qDebug() << "moving: " << info.absoluteFilePath() << " to: " << destDir.absolutePath() + relativePath; + qDebug() << "moving: " << info.absoluteFilePath() << " to: " << destDir.absolutePath() + relativePath; result = moveFile(info.absoluteFilePath(), destDir.absolutePath() + relativePath); } @@ -389,6 +389,19 @@ void Wizard::UnshieldWorker::extract() { while (!getTribunalDone()) { + QDir tribunal(disk); + + if (!tribunal.cd(QLatin1String("Tribunal"))) { + qDebug() << "not found on cd!"; + QReadLocker locker(&mLock); + emit requestFileDialog(QLatin1String("Tribunal")); + mWait.wait(&mLock); + + } else if (tribunal.exists(QLatin1String("data1.hdr"))) { + qDebug() << "Exists! " << tribunal.absolutePath(); + setTribunalPath(tribunal.absolutePath()); + } + if (getTribunalPath().isEmpty()) { qDebug() << "request file dialog"; QReadLocker locker(&mLock); @@ -396,10 +409,12 @@ void Wizard::UnshieldWorker::extract() mWait.wait(&mLock); } - if (!getTribunalPath().isEmpty()) { - disk.setPath(getTribunalPath()); + // Make sure the dir is up-to-date + tribunal.setPath(getTribunalPath()); - if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa"))) + if (!getTribunalPath().isEmpty()) { + + if (!findFile(tribunal.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa"))) { qDebug() << "found"; QReadLocker locker(&mLock); @@ -421,6 +436,21 @@ void Wizard::UnshieldWorker::extract() { while (!getBloodmoonDone()) { + QDir bloodmoon(disk); + + qDebug() << "Test!: " << bloodmoon.absolutePath(); + + if (!bloodmoon.cd(QLatin1String("Bloodmoon"))) { + qDebug() << "not found on cd!"; + QReadLocker locker(&mLock); + emit requestFileDialog(QLatin1String("Bloodmoon")); + mWait.wait(&mLock); + + } else if (bloodmoon.exists(QLatin1String("data1.hdr"))) { + qDebug() << "Exists! " << bloodmoon.absolutePath(); + setBloodmoonPath(bloodmoon.absolutePath()); + } + if (getBloodmoonPath().isEmpty()) { qDebug() << "request file dialog"; QReadLocker locker(&mLock); @@ -428,35 +458,38 @@ void Wizard::UnshieldWorker::extract() mWait.wait(&mLock); } - if (!getBloodmoonPath().isEmpty()) { - disk.setPath(getBloodmoonPath()); + // Make sure the dir is up-to-date + bloodmoon.setPath(getBloodmoonPath()); - if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) - { - QReadLocker locker(&mLock); - emit requestFileDialog(QLatin1String("Bloodmoon")); - mWait.wait(&mLock); + if (!findFile(bloodmoon.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) + { + QReadLocker locker(&mLock); + emit requestFileDialog(QLatin1String("Bloodmoon")); + mWait.wait(&mLock); + } else { + if (installBloodmoon()) { + setBloodmoonDone(true); } else { - if (installBloodmoon()) { - setBloodmoonDone(true); - } else { - qDebug() << "Erorr installing Bloodmoon"; - return; - } + qDebug() << "Erorr installing Bloodmoon"; + return; } } } } + // Remove the temporary directory + removeDirectory(mPath + QDir::separator() + QLatin1String("extract-temp")); + + // Fill the progress bar int total = 0; - if (mInstallMorrowind) + if (getInstallMorrowind()) total = 100; - if (mInstallTribunal) + if (getInstallTribunal()) total = total + 100; - if (mInstallBloodmoon) + if (getInstallBloodmoon()) total = total + 100; emit textChanged(tr("Installation finished!")); @@ -523,8 +556,6 @@ bool Wizard::UnshieldWorker::installMorrowind() QFileInfo info(iniPath); - qDebug() << info.absoluteFilePath() << getPath(); - if (info.exists()) { emit textChanged(tr("Extracting: Morrowind.ini")); moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); @@ -579,7 +610,8 @@ bool Wizard::UnshieldWorker::installTribunal() // TODO: Throw error; // Move the files from the temporary path to the destination folder emit textChanged(tr("Moving installation files")); - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { + if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) + { qDebug() << "failed to move files!"; return false; } @@ -687,8 +719,6 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp if (getTribunalDone()) progress = progress + 100; - qDebug() << progress << counter << unshield_file_count(unshield); - emit textChanged(tr("Extracting: %1").arg(QString::fromLatin1(unshield_file_name(unshield, index)))); emit progressChanged(progress); From 802448b217477578a20d5543194623455b45c1b2 Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 27 Jan 2014 20:14:02 +0100 Subject: [PATCH 023/303] Added a settings page to the launcher --- apps/launcher/CMakeLists.txt | 4 + apps/launcher/maindialog.cpp | 28 +-- apps/launcher/maindialog.hpp | 7 + apps/launcher/settingspage.cpp | 18 ++ apps/launcher/settingspage.hpp | 20 +++ .../icons/tango/{ => 16x16}/document-new.png | Bin .../icons/tango/{ => 16x16}/edit-copy.png | Bin .../icons/tango/{ => 16x16}/edit-delete.png | Bin .../icons/tango/{ => 16x16}/go-bottom.png | Bin .../icons/tango/{ => 16x16}/go-down.png | Bin .../icons/tango/{ => 16x16}/go-top.png | Bin .../icons/tango/{ => 16x16}/go-up.png | Bin .../icons/tango/48x48/preferences-system.png | Bin 0 -> 3718 bytes .../icons/tango/{ => 48x48}/video-display.png | Bin files/launcher/icons/tango/index.theme | 7 +- files/launcher/launcher.qrc | 17 +- files/ui/mainwindow.ui | 10 +- files/ui/settingspage.ui | 159 ++++++++++++++++++ 18 files changed, 248 insertions(+), 22 deletions(-) create mode 100644 apps/launcher/settingspage.cpp create mode 100644 apps/launcher/settingspage.hpp rename files/launcher/icons/tango/{ => 16x16}/document-new.png (100%) rename files/launcher/icons/tango/{ => 16x16}/edit-copy.png (100%) rename files/launcher/icons/tango/{ => 16x16}/edit-delete.png (100%) rename files/launcher/icons/tango/{ => 16x16}/go-bottom.png (100%) rename files/launcher/icons/tango/{ => 16x16}/go-down.png (100%) rename files/launcher/icons/tango/{ => 16x16}/go-top.png (100%) rename files/launcher/icons/tango/{ => 16x16}/go-up.png (100%) create mode 100644 files/launcher/icons/tango/48x48/preferences-system.png rename files/launcher/icons/tango/{ => 48x48}/video-display.png (100%) create mode 100644 files/ui/settingspage.ui diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index 2794744068..d24c8e3770 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -5,6 +5,7 @@ set(LAUNCHER maindialog.cpp playpage.cpp textslotmsgbox.cpp + settingspage.cpp settings/graphicssettings.cpp @@ -25,6 +26,7 @@ set(LAUNCHER_HEADER maindialog.hpp playpage.hpp textslotmsgbox.hpp + settingspage.hpp settings/graphicssettings.hpp @@ -45,6 +47,7 @@ set(LAUNCHER_HEADER_MOC maindialog.hpp playpage.hpp textslotmsgbox.hpp + settingspage.hpp utils/textinputdialog.hpp utils/checkablemessagebox.hpp @@ -64,6 +67,7 @@ set(LAUNCHER_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 ) source_group(launcher FILES ${LAUNCHER} ${LAUNCHER_HEADER}) diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 4012a1fbd5..f81d15e9c5 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -23,6 +23,7 @@ #include "playpage.hpp" #include "graphicspage.hpp" #include "datafilespage.hpp" +#include "settingspage.hpp" Launcher::MainDialog::MainDialog(QWidget *parent) : mGameSettings(mCfgMgr), QMainWindow (parent) @@ -75,27 +76,30 @@ void Launcher::MainDialog::createIcons() if (!QIcon::hasThemeIcon("document-new")) QIcon::setThemeName("tango"); - // We create a fallback icon because the default fallback doesn't work - QIcon graphicsIcon = QIcon(":/icons/tango/video-display.png"); - 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 *graphicsButton = new QListWidgetItem(iconWidget); - graphicsButton->setIcon(QIcon::fromTheme("video-display", graphicsIcon)); - graphicsButton->setText(tr("Graphics")); - graphicsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom | Qt::AlignAbsolute); - graphicsButton->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::fromTheme("video-display")); + 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::fromTheme("preferences-system")); + settingsButton->setText(tr("Settings")); + settingsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); + settingsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + connect(iconWidget, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), this, SLOT(changePage(QListWidgetItem*,QListWidgetItem*))); @@ -105,8 +109,9 @@ void Launcher::MainDialog::createIcons() void Launcher::MainDialog::createPages() { mPlayPage = new PlayPage(this); - mGraphicsPage = new GraphicsPage(mCfgMgr, mGraphicsSettings, this); mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this); + mGraphicsPage = new GraphicsPage(mCfgMgr, mGraphicsSettings, this); + mSettingsPage = new SettingsPage(this); // Set the combobox of the play page to imitate the combobox on the datafilespage mPlayPage->setProfilesModel(mDataFilesPage->profilesModel()); @@ -114,8 +119,9 @@ void Launcher::MainDialog::createPages() // Add the pages to the stacked widget pagesWidget->addWidget(mPlayPage); - pagesWidget->addWidget(mGraphicsPage); pagesWidget->addWidget(mDataFilesPage); + pagesWidget->addWidget(mGraphicsPage); + pagesWidget->addWidget(mSettingsPage); // Select the first page iconWidget->setCurrentItem(iconWidget->item(0), QItemSelectionModel::Select); diff --git a/apps/launcher/maindialog.hpp b/apps/launcher/maindialog.hpp index a4fb4cd9a6..718acb5a9b 100644 --- a/apps/launcher/maindialog.hpp +++ b/apps/launcher/maindialog.hpp @@ -13,6 +13,10 @@ #include "ui_mainwindow.h" class QListWidgetItem; +class QStackedWidget; +class QStringList; +class QStringListModel; +class QString; namespace Launcher { @@ -20,6 +24,7 @@ namespace Launcher class GraphicsPage; class DataFilesPage; class UnshieldThread; + class SettingsPage; #ifndef WIN32 bool expansions(Launcher::UnshieldThread& cd); @@ -58,6 +63,8 @@ namespace Launcher PlayPage *mPlayPage; GraphicsPage *mGraphicsPage; DataFilesPage *mDataFilesPage; + SettingsPage *mSettingsPage; + Files::ConfigurationManager mCfgMgr; diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp new file mode 100644 index 0000000000..96dd6e5299 --- /dev/null +++ b/apps/launcher/settingspage.cpp @@ -0,0 +1,18 @@ +#include "settingspage.hpp" + +Launcher::SettingsPage::SettingsPage(QWidget *parent) : QWidget(parent) +{ + setupUi(this); + + QStringList languages; + languages << "English" + << "French" + << "German" + << "Italian" + << "Polish" + << "Russian" + << "Spanish"; + + languageComboBox->addItems(languages); +} + diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp new file mode 100644 index 0000000000..c87f6e46ff --- /dev/null +++ b/apps/launcher/settingspage.hpp @@ -0,0 +1,20 @@ +#ifndef SETTINGSPAGE_HPP +#define SETTINGSPAGE_HPP + +#include + +#include "ui_settingspage.h" + +namespace Launcher +{ + + class SettingsPage : public QWidget, private Ui::SettingsPage + { + Q_OBJECT + public: + SettingsPage(QWidget *parent = 0); + + }; +} + +#endif // SETTINGSPAGE_HPP diff --git a/files/launcher/icons/tango/document-new.png b/files/launcher/icons/tango/16x16/document-new.png similarity index 100% rename from files/launcher/icons/tango/document-new.png rename to files/launcher/icons/tango/16x16/document-new.png diff --git a/files/launcher/icons/tango/edit-copy.png b/files/launcher/icons/tango/16x16/edit-copy.png similarity index 100% rename from files/launcher/icons/tango/edit-copy.png rename to files/launcher/icons/tango/16x16/edit-copy.png diff --git a/files/launcher/icons/tango/edit-delete.png b/files/launcher/icons/tango/16x16/edit-delete.png similarity index 100% rename from files/launcher/icons/tango/edit-delete.png rename to files/launcher/icons/tango/16x16/edit-delete.png diff --git a/files/launcher/icons/tango/go-bottom.png b/files/launcher/icons/tango/16x16/go-bottom.png similarity index 100% rename from files/launcher/icons/tango/go-bottom.png rename to files/launcher/icons/tango/16x16/go-bottom.png diff --git a/files/launcher/icons/tango/go-down.png b/files/launcher/icons/tango/16x16/go-down.png similarity index 100% rename from files/launcher/icons/tango/go-down.png rename to files/launcher/icons/tango/16x16/go-down.png diff --git a/files/launcher/icons/tango/go-top.png b/files/launcher/icons/tango/16x16/go-top.png similarity index 100% rename from files/launcher/icons/tango/go-top.png rename to files/launcher/icons/tango/16x16/go-top.png diff --git a/files/launcher/icons/tango/go-up.png b/files/launcher/icons/tango/16x16/go-up.png similarity index 100% rename from files/launcher/icons/tango/go-up.png rename to files/launcher/icons/tango/16x16/go-up.png diff --git a/files/launcher/icons/tango/48x48/preferences-system.png b/files/launcher/icons/tango/48x48/preferences-system.png new file mode 100644 index 0000000000000000000000000000000000000000..a1620e991663d37d6e95ed11be7abb9927d73dfc GIT binary patch literal 3718 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 literal 0 HcmV?d00001 diff --git a/files/launcher/icons/tango/video-display.png b/files/launcher/icons/tango/48x48/video-display.png similarity index 100% rename from files/launcher/icons/tango/video-display.png rename to files/launcher/icons/tango/48x48/video-display.png diff --git a/files/launcher/icons/tango/index.theme b/files/launcher/icons/tango/index.theme index 1f54489ebb..8ec560f856 100644 --- a/files/launcher/icons/tango/index.theme +++ b/files/launcher/icons/tango/index.theme @@ -2,7 +2,10 @@ Name=Tango Comment=Tango Theme Inherits=default -Directories=16x16 +Directories=16x16,48x48 [16x16] -Size=16 \ No newline at end of file +Size=16 + +[48x48] +Size=48 \ No newline at end of file diff --git a/files/launcher/launcher.qrc b/files/launcher/launcher.qrc index 19b1c5a6f4..4f55fccd5b 100644 --- a/files/launcher/launcher.qrc +++ b/files/launcher/launcher.qrc @@ -9,13 +9,14 @@ icons/tango/index.theme - icons/tango/video-display.png - icons/tango/document-new.png - icons/tango/edit-copy.png - icons/tango/edit-delete.png - icons/tango/go-bottom.png - icons/tango/go-down.png - icons/tango/go-top.png - icons/tango/go-up.png + icons/tango/48x48/video-display.png + icons/tango/48x48/preferences-system.png + icons/tango/16x16/document-new.png + icons/tango/16x16/edit-copy.png + icons/tango/16x16/edit-delete.png + icons/tango/16x16/go-bottom.png + icons/tango/16x16/go-down.png + icons/tango/16x16/go-top.png + icons/tango/16x16/go-up.png diff --git a/files/ui/mainwindow.ui b/files/ui/mainwindow.ui index a1dfb172b2..368e5cc5f7 100644 --- a/files/ui/mainwindow.ui +++ b/files/ui/mainwindow.ui @@ -2,9 +2,17 @@ MainWindow + + + 0 + 0 + 635 + 575 + + - 575 + 635 535 diff --git a/files/ui/settingspage.ui b/files/ui/settingspage.ui new file mode 100644 index 0000000000..95b62761e6 --- /dev/null +++ b/files/ui/settingspage.ui @@ -0,0 +1,159 @@ + + + SettingsPage + + + + 0 + 0 + 518 + 401 + + + + Form + + + + + + General + + + + + + Morrowind installation language: + + + + + + + + 250 + 0 + + + + + + + + + + + Morrowind Installation Wizard + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Run &Installation Wizard + + + + + + + + + + Morrowind Settings Importer + + + + + + + + File to import settings from: + + + + + + + + /home/user/.local/share/openmw/data/Morrowind.ini + + + + + + + + Browse... + + + + + + + + + Import previously selected add-ons (creates a new Content List) + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Run &Settings Importer + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + From 2736527e5a37ecf15b38474dbfd5624b61898394 Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 27 Jan 2014 22:54:14 +0100 Subject: [PATCH 024/303] Did some cleanup work on the unshield thread: more code re-use --- apps/wizard/installationpage.cpp | 59 +-- apps/wizard/installationpage.hpp | 3 +- apps/wizard/unshield/unshieldworker.cpp | 548 +++++++++--------------- apps/wizard/unshield/unshieldworker.hpp | 42 +- 4 files changed, 255 insertions(+), 397 deletions(-) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 8690cd2bc7..47d10c45f9 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -8,7 +8,6 @@ #include "mainwizard.hpp" #include "inisettings.hpp" -#include "unshield/unshieldworker.hpp" Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : QWizardPage(wizard), @@ -58,9 +57,10 @@ void Wizard::InstallationPage::startInstallation() QThread *thread = new QThread(); mUnshield = new UnshieldWorker(); - mUnshield->moveToThread(thread); + qRegisterMetaType("Wizard::Component"); + connect(thread, SIGNAL(started()), mUnshield, SLOT(extract())); @@ -88,30 +88,30 @@ void Wizard::InstallationPage::startInstallation() connect(mUnshield, SIGNAL(progressChanged(int)), installProgressBar, SLOT(setValue(int)), Qt::QueuedConnection); - connect(mUnshield, SIGNAL(requestFileDialog(QString)), - this, SLOT(showFileDialog(QString)), Qt::QueuedConnection); + connect(mUnshield, SIGNAL(requestFileDialog(Wizard::Component)), + this, SLOT(showFileDialog(Wizard::Component)), Qt::QueuedConnection); if (field("installation.new").toBool() == true) { // Always install Morrowind - mUnshield->setInstallMorrowind(true); + mUnshield->setInstallComponent(Wizard::Component_Morrowind, true); if (components.contains(QLatin1String("Tribunal"))) - mUnshield->setInstallTribunal(true); + mUnshield->setInstallComponent(Wizard::Component_Tribunal, true); if (components.contains(QLatin1String("Bloodmoon"))) - mUnshield->setInstallBloodmoon(true); + mUnshield->setInstallComponent(Wizard::Component_Bloodmoon, true); } else { // Morrowind should already be installed - mUnshield->setInstallMorrowind(false); + mUnshield->setInstallComponent(Wizard::Component_Morrowind, false); if (components.contains(QLatin1String("Tribunal")) - && mWizard->mInstallations[path]->hasTribunal == false) - mUnshield->setInstallTribunal(true); + && !mWizard->mInstallations[path]->hasTribunal) + mUnshield->setInstallComponent(Wizard::Component_Tribunal, true); if (components.contains(QLatin1String("Bloodmoon")) - && mWizard->mInstallations[path]->hasBloodmoon == false) - mUnshield->setInstallBloodmoon(true); + && !mWizard->mInstallations[path]->hasBloodmoon) + mUnshield->setInstallComponent(Wizard::Component_Bloodmoon, true); // Set the location of the Morrowind.ini to update mUnshield->setIniPath(mWizard->mInstallations[path]->iniPath); @@ -136,38 +136,21 @@ void Wizard::InstallationPage::startInstallation() thread->start(); } -void Wizard::InstallationPage::showFileDialog(const QString &component) +void Wizard::InstallationPage::showFileDialog(Wizard::Component component) { - QString fileName; - - if (field("installation.new").toBool() == true) - { - fileName = QFileDialog::getOpenFileName( + QString fileName = QFileDialog::getOpenFileName( this, - tr("Select %0 installation file").arg(component), + tr("Select installation file"), QDir::rootPath(), tr("InstallShield header files (*.hdr)")); - if (fileName.isEmpty()) { - qDebug() << "Cancel was clicked!"; - return; - } - - QFileInfo info(fileName); - - if (component == QLatin1String("Morrowind")) - { - mUnshield->setMorrowindPath(info.absolutePath()); - } - else if (component == QLatin1String("Tribunal")) - { - mUnshield->setTribunalPath(info.absolutePath()); - } - else if (component == QLatin1String("Bloodmoon")) - { - mUnshield->setBloodmoonPath(info.absolutePath()); - } + if (fileName.isEmpty()) { + qDebug() << "Cancel was clicked!"; + return; } + + QFileInfo info(fileName); + mUnshield->setComponentPath(component, info.absolutePath()); } void Wizard::InstallationPage::installationFinished() diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index e125eadc4a..3989177abd 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -3,6 +3,7 @@ #include +#include "unshield/unshieldworker.hpp" #include "ui_installationpage.h" #include "inisettings.hpp" @@ -33,7 +34,7 @@ namespace Wizard void startInstallation(); private slots: - void showFileDialog(const QString &component); + void showFileDialog(Wizard::Component component); void installationFinished(); void installationError(const QString &text); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 2ab68dde09..9c635818be 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -14,8 +14,6 @@ #include #include -#include - Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : QObject(parent), mIniSettings() @@ -46,81 +44,106 @@ Wizard::UnshieldWorker::~UnshieldWorker() } -void Wizard::UnshieldWorker::setInstallMorrowind(bool install) +void Wizard::UnshieldWorker::setInstallComponent(Wizard::Component component, bool install) { QWriteLocker writeLock(&mLock); - mInstallMorrowind = install; + switch (component) { + + case Wizard::Component_Morrowind: + mInstallMorrowind = install; + break; + case Wizard::Component_Tribunal: + mInstallTribunal = install; + break; + case Wizard::Component_Bloodmoon: + mInstallBloodmoon = install; + break; + } } -void Wizard::UnshieldWorker::setInstallTribunal(bool install) +bool Wizard::UnshieldWorker::getInstallComponent(Component component) +{ + QReadLocker readLock(&mLock); + switch (component) { + + case Wizard::Component_Morrowind: + return mInstallMorrowind; + case Wizard::Component_Tribunal: + return mInstallTribunal; + case Wizard::Component_Bloodmoon: + return mInstallBloodmoon; + } + + return false; +} + +void Wizard::UnshieldWorker::setComponentPath(Wizard::Component component, const QString &path) +{ + QWriteLocker writeLock(&mLock); + switch (component) { + + case Wizard::Component_Morrowind: + mMorrowindPath = path; + break; + case Wizard::Component_Tribunal: + mTribunalPath = path; + break; + case Wizard::Component_Bloodmoon: + mBloodmoonPath = path; + break; + } + + mWait.wakeAll(); +} + +QString Wizard::UnshieldWorker::getComponentPath(Component component) +{ + QReadLocker readLock(&mLock); + switch (component) { + + case Wizard::Component_Morrowind: + return mMorrowindPath; + case Wizard::Component_Tribunal: + return mTribunalPath; + case Wizard::Component_Bloodmoon: + return mBloodmoonPath; + } + + return QString(); +} + +void Wizard::UnshieldWorker::setComponentDone(Component component, bool done) { QWriteLocker writeLock(&mLock); - mInstallTribunal = install; + switch (component) { + + case Wizard::Component_Morrowind: + mMorrowindDone = done; + break; + case Wizard::Component_Tribunal: + mTribunalDone = done; + break; + case Wizard::Component_Bloodmoon: + mBloodmoonDone = done; + break; + } } -void Wizard::UnshieldWorker::setInstallBloodmoon(bool install) -{ - QWriteLocker writeLock(&mLock); - mInstallBloodmoon = install; -} - -bool Wizard::UnshieldWorker::getInstallMorrowind() +bool Wizard::UnshieldWorker::getComponentDone(Component component) { QReadLocker readLock(&mLock); - return mInstallMorrowind; -} + switch (component) + { -bool Wizard::UnshieldWorker::getInstallTribunal() -{ - QReadLocker readLock(&mLock); - return mInstallTribunal; -} + case Wizard::Component_Morrowind: + return mMorrowindDone; + case Wizard::Component_Tribunal: + return mTribunalDone; + case Wizard::Component_Bloodmoon: + return mBloodmoonDone; + } -bool Wizard::UnshieldWorker::getInstallBloodmoon() -{ - QReadLocker readLock(&mLock); - return mInstallBloodmoon; -} - -void Wizard::UnshieldWorker::setMorrowindPath(const QString &path) -{ - QWriteLocker writeLock(&mLock); - mMorrowindPath = path; - mWait.wakeAll(); -} - -void Wizard::UnshieldWorker::setTribunalPath(const QString &path) -{ - QWriteLocker writeLock(&mLock); - mTribunalPath = path; - mWait.wakeAll(); - -} - -void Wizard::UnshieldWorker::setBloodmoonPath(const QString &path) -{ - QWriteLocker writeLock(&mLock); - mBloodmoonPath = path; - mWait.wakeAll(); - -} - -QString Wizard::UnshieldWorker::getMorrowindPath() -{ - QReadLocker readLock(&mLock); - return mMorrowindPath; -} - -QString Wizard::UnshieldWorker::getTribunalPath() -{ - QReadLocker readLock(&mLock); - return mTribunalPath; -} - -QString Wizard::UnshieldWorker::getBloodmoonPath() -{ - QReadLocker readLock(&mLock); - return mBloodmoonPath; + return false; } void Wizard::UnshieldWorker::setPath(const QString &path) @@ -153,42 +176,6 @@ void Wizard::UnshieldWorker::setIniCodec(QTextCodec *codec) mIniCodec = codec; } -void Wizard::UnshieldWorker::setMorrowindDone(bool done) -{ - QWriteLocker writeLock(&mLock); - mMorrowindDone = done; -} - -void Wizard::UnshieldWorker::setTribunalDone(bool done) -{ - QWriteLocker writeLock(&mLock); - mTribunalDone = done; -} - -void Wizard::UnshieldWorker::setBloodmoonDone(bool done) -{ - QWriteLocker writeLock(&mLock); - mBloodmoonDone = done; -} - -bool Wizard::UnshieldWorker::getMorrowindDone() -{ - QReadLocker readLock(&mLock); - return mMorrowindDone; -} - -bool Wizard::UnshieldWorker::getTribunalDone() -{ - QReadLocker readLock(&mLock); - return mTribunalDone; -} - -bool Wizard::UnshieldWorker::getBloodmoonDone() -{ - QReadLocker readLock(&mLock); - return mBloodmoonDone; -} - void Wizard::UnshieldWorker::setupSettings() { // Create Morrowind.ini settings map @@ -287,8 +274,7 @@ bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString if (info.isDir()) { result = moveDirectory(info.absoluteFilePath(), destDir.absolutePath() + relativePath); } else { - qDebug() << "moving: " << info.absoluteFilePath() << " to: " << destDir.absolutePath() + relativePath; - +// qDebug() << "moving: " << info.absoluteFilePath() << " to: " << destDir.absolutePath() + relativePath; result = moveFile(info.absoluteFilePath(), destDir.absolutePath() + relativePath); } } @@ -346,36 +332,35 @@ void Wizard::UnshieldWorker::installDirectories(const QString &source) } - void Wizard::UnshieldWorker::extract() { qDebug() << "extract!"; QDir disk; - if (getInstallMorrowind()) + if (getInstallComponent(Wizard::Component_Morrowind)) { - while (!getMorrowindDone()) + while (!getComponentDone(Wizard::Component_Morrowind)) { - if (getMorrowindPath().isEmpty()) { + if (getComponentPath(Wizard::Component_Morrowind).isEmpty()) { qDebug() << "request file dialog"; QReadLocker readLock(&mLock); - emit requestFileDialog(QLatin1String("Morrowind")); + emit requestFileDialog(Wizard::Component_Morrowind); mWait.wait(&mLock); } - if (!getMorrowindPath().isEmpty()) { - disk.setPath(getMorrowindPath()); + if (!getComponentPath(Wizard::Component_Morrowind).isEmpty()) { + disk.setPath(getComponentPath(Wizard::Component_Morrowind)); if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Morrowind.bsa")) | findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa")) | findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) { QReadLocker readLock(&mLock); - emit requestFileDialog(QLatin1String("Morrowind")); + emit requestFileDialog(Wizard::Component_Morrowind); mWait.wait(&mLock); } else { - if (installMorrowind()) { - setMorrowindDone(true); + if (installComponent(Wizard::Component_Morrowind)) { + setComponentDone(Wizard::Component_Morrowind, true); } else { qDebug() << "Erorr installing Morrowind"; return; @@ -385,111 +370,29 @@ void Wizard::UnshieldWorker::extract() } } - if (getInstallTribunal()) + if (getInstallComponent(Wizard::Component_Tribunal)) { - while (!getTribunalDone()) - { - QDir tribunal(disk); - - if (!tribunal.cd(QLatin1String("Tribunal"))) { - qDebug() << "not found on cd!"; - QReadLocker locker(&mLock); - emit requestFileDialog(QLatin1String("Tribunal")); - mWait.wait(&mLock); - - } else if (tribunal.exists(QLatin1String("data1.hdr"))) { - qDebug() << "Exists! " << tribunal.absolutePath(); - setTribunalPath(tribunal.absolutePath()); - } - - if (getTribunalPath().isEmpty()) { - qDebug() << "request file dialog"; - QReadLocker locker(&mLock); - emit requestFileDialog(QLatin1String("Tribunal")); - mWait.wait(&mLock); - } - - // Make sure the dir is up-to-date - tribunal.setPath(getTribunalPath()); - - if (!getTribunalPath().isEmpty()) { - - if (!findFile(tribunal.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa"))) - { - qDebug() << "found"; - QReadLocker locker(&mLock); - emit requestFileDialog(QLatin1String("Tribunal")); - mWait.wait(&mLock); - } else { - if (installTribunal()) { - setTribunalDone(true); - } else { - qDebug() << "Erorr installing Tribunal"; - return; - } - } - } - } + setupAddon(Wizard::Component_Tribunal); } - if (getInstallBloodmoon()) + if (getInstallComponent(Wizard::Component_Bloodmoon)) { - while (!getBloodmoonDone()) - { - QDir bloodmoon(disk); - - qDebug() << "Test!: " << bloodmoon.absolutePath(); - - if (!bloodmoon.cd(QLatin1String("Bloodmoon"))) { - qDebug() << "not found on cd!"; - QReadLocker locker(&mLock); - emit requestFileDialog(QLatin1String("Bloodmoon")); - mWait.wait(&mLock); - - } else if (bloodmoon.exists(QLatin1String("data1.hdr"))) { - qDebug() << "Exists! " << bloodmoon.absolutePath(); - setBloodmoonPath(bloodmoon.absolutePath()); - } - - if (getBloodmoonPath().isEmpty()) { - qDebug() << "request file dialog"; - QReadLocker locker(&mLock); - emit requestFileDialog(QLatin1String("Bloodmoon")); - mWait.wait(&mLock); - } - - // Make sure the dir is up-to-date - bloodmoon.setPath(getBloodmoonPath()); - - if (!findFile(bloodmoon.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) - { - QReadLocker locker(&mLock); - emit requestFileDialog(QLatin1String("Bloodmoon")); - mWait.wait(&mLock); - } else { - if (installBloodmoon()) { - setBloodmoonDone(true); - } else { - qDebug() << "Erorr installing Bloodmoon"; - return; - } - } - } + setupAddon(Wizard::Component_Bloodmoon); } // Remove the temporary directory - removeDirectory(mPath + QDir::separator() + QLatin1String("extract-temp")); + removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); // Fill the progress bar int total = 0; - if (getInstallMorrowind()) + if (getInstallComponent(Wizard::Component_Morrowind)) total = 100; - if (getInstallTribunal()) + if (getInstallComponent(Wizard::Component_Tribunal)) total = total + 100; - if (getInstallBloodmoon()) + if (getInstallComponent(Wizard::Component_Bloodmoon)) total = total + 100; emit textChanged(tr("Installation finished!")); @@ -499,137 +402,87 @@ void Wizard::UnshieldWorker::extract() qDebug() << "installation finished!"; } -bool Wizard::UnshieldWorker::installMorrowind() +void Wizard::UnshieldWorker::setupAddon(Component component) { - qDebug() << "install morrowind!"; - emit textChanged(QLatin1String("Installing Morrowind")); - - QDir disk(getMorrowindPath()); - - if (!disk.exists()) { - qDebug() << "getMorrowindPath: " << getMorrowindPath(); - return false; - } - - // Create temporary extract directory - // TODO: Use QTemporaryDir in Qt 5.0 - QString tempPath(getPath() + QDir::separator() + QLatin1String("extract-temp")); - QDir temp; - - // Make sure the temporary folder is empty - removeDirectory(tempPath); - - if (!temp.mkpath(tempPath)) { - qDebug() << "Can't make path"; - return false; - } - - temp.setPath(tempPath); - - if (!temp.mkdir(QLatin1String("morrowind"))) { - qDebug() << "Can't make dir"; - return false; - } - - if (!temp.cd(QLatin1String("morrowind"))) { - qDebug() << "Can't cd to dir"; - return false; - } - - // Extract the installation files - extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); - - // TODO: Throw error; - // Move the files from the temporary path to the destination folder - emit textChanged(tr("Moving installation files")); - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { - qDebug() << "failed to move files!"; - return false; - } - - // Install files outside of cab archives - installDirectories(disk.absolutePath()); - - // Copy Morrowind configuration file - QString iniPath(temp.absoluteFilePath(QLatin1String("App Executables"))); - iniPath.append(QDir::separator() + QLatin1String("Morrowind.ini")); - - QFileInfo info(iniPath); - - if (info.exists()) { - emit textChanged(tr("Extracting: Morrowind.ini")); - moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); - } else { - qDebug() << "Could not find ini file!"; - return false; - } - - emit textChanged(tr("Morrowind installation finished!")); - return true; -} - -bool Wizard::UnshieldWorker::installTribunal() -{ - emit textChanged(QLatin1String("Installing Tribunal")); - - QDir disk(getTribunalPath()); - - if (!disk.exists()) { - qDebug() << "disk does not exist! " << disk.absolutePath() << getTribunalPath(); - return false; - } - - // Create temporary extract directory - // TODO: Use QTemporaryDir in Qt 5.0 - QString tempPath(getPath() + QDir::separator() + QLatin1String("extract-temp")); - QDir temp; - - // Make sure the temporary folder is empty - removeDirectory(tempPath); - - if (!temp.mkpath(tempPath)) { - qDebug() << "Can't make path"; - return false; - } - - temp.setPath(tempPath); - - if (!temp.mkdir(QLatin1String("tribunal"))) { - qDebug() << "Can't make dir"; - return false; - } - - if (!temp.cd(QLatin1String("tribunal"))) { - qDebug() << "Can't cd to dir"; - return false; - } - - // Extract the installation files - extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); - - // TODO: Throw error; - // Move the files from the temporary path to the destination folder - emit textChanged(tr("Moving installation files")); - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) + while (!getComponentDone(component)) { - qDebug() << "failed to move files!"; - return false; + QDir disk(getComponentPath(Wizard::Component_Morrowind)); + QString name; + if (component == Wizard::Component_Tribunal) + name = QLatin1String("Tribunal"); + + if (component == Wizard::Component_Bloodmoon) + name = QLatin1String("Bloodmoon"); + + if (name.isEmpty()) + return; // Not a valid addon + + if (!disk.cd(name)) { + qDebug() << "not found on cd!"; + QReadLocker locker(&mLock); + emit requestFileDialog(component); + mWait.wait(&mLock); + + } else if (disk.exists(QLatin1String("data1.hdr"))) { + qDebug() << "Exists! " << disk.absolutePath(); + setComponentPath(component, disk.absolutePath()); + } + + if (getComponentPath(component).isEmpty()) { + qDebug() << "request file dialog"; + QReadLocker locker(&mLock); + emit requestFileDialog(Wizard::Component_Tribunal); + mWait.wait(&mLock); + } + + // Make sure the dir is up-to-date + disk.setPath(getComponentPath(component)); + + if (!getComponentPath(component).isEmpty()) { + + if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) + { + QReadLocker locker(&mLock); + emit requestFileDialog(component); + mWait.wait(&mLock); + } else { + // Now do the actual installing + if (installComponent(component)) { + setComponentDone(component, true); + } else { + qDebug() << "Error installing " << name; + return; + } + } + } } - - // Install files outside of cab archives - installDirectories(disk.absolutePath()); - - emit textChanged(tr("Tribunal installation finished!")); - return true; } -bool Wizard::UnshieldWorker::installBloodmoon() +bool Wizard::UnshieldWorker::installComponent(Component component) { - emit textChanged(QLatin1String("Installing Bloodmoon")); + QString name; + switch (component) { - QDir disk(getBloodmoonPath()); + case Wizard::Component_Morrowind: + name = QLatin1String("Morrowind"); + break; + case Wizard::Component_Tribunal: + name = QLatin1String("Tribunal"); + break; + case Wizard::Component_Bloodmoon: + name = QLatin1String("Bloodmoon"); + break; + } + + if (name.isEmpty()) + return false; + + emit textChanged(tr("Installing %0").arg(name)); + + QDir disk(getComponentPath(component)); if (!disk.exists()) { + qDebug() << "Component path not set: " << getComponentPath(Wizard::Component_Morrowind); return false; } @@ -648,12 +501,12 @@ bool Wizard::UnshieldWorker::installBloodmoon() temp.setPath(tempPath); - if (!temp.mkdir(QLatin1String("bloodmoon"))) { + if (!temp.mkdir(name)) { qDebug() << "Can't make dir"; return false; } - if (!temp.cd(QLatin1String("bloodmoon"))) { + if (!temp.cd(name)) { qDebug() << "Can't cd to dir"; return false; } @@ -672,18 +525,39 @@ bool Wizard::UnshieldWorker::installBloodmoon() // Install files outside of cab archives installDirectories(disk.absolutePath()); - QFileInfo patch(temp.absoluteFilePath(QLatin1String("Tribunal Patch") + QDir::separator() + QLatin1String("Tribunal.esm"))); - QFileInfo original(getPath() + QDir::separator() + QLatin1String("Tribunal.esm")); + if (component == Wizard::Component_Morrowind) + { + // Copy Morrowind configuration file + QString iniPath(temp.absoluteFilePath(QLatin1String("App Executables"))); + iniPath.append(QDir::separator() + QLatin1String("Morrowind.ini")); - if (original.exists() && patch.exists()) { - emit textChanged(tr("Extracting: Tribunal patch")); - copyFile(patch.absoluteFilePath(), original.absoluteFilePath()); + QFileInfo info(iniPath); + + if (info.exists()) { + emit textChanged(tr("Extracting: Morrowind.ini")); + moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); + } else { + qDebug() << "Could not find ini file!"; + return false; + } } - emit textChanged(tr("Bloodmoon installation finished!")); - return true; -} + if (component == Wizard::Component_Bloodmoon) + { + QFileInfo patch(temp.absoluteFilePath(QLatin1String("Tribunal Patch") + QDir::separator() + QLatin1String("Tribunal.esm"))); + QFileInfo original(getPath() + QDir::separator() + QLatin1String("Tribunal.esm")); + if (original.exists() && patch.exists()) { + emit textChanged(tr("Extracting: Tribunal patch")); + copyFile(patch.absoluteFilePath(), original.absoluteFilePath()); + } + + } + + emit textChanged(tr("%0 installation finished!").arg(name)); + return true; + +} bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter) { @@ -713,10 +587,10 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp // Calculate the percentage done int progress = (((float) counter / (float) unshield_file_count(unshield)) * 100); - if (getMorrowindDone()) + if (getComponentDone(Wizard::Component_Morrowind)) progress = progress + 100; - if (getTribunalDone()) + if (getComponentDone(Wizard::Component_Tribunal)) progress = progress + 100; emit textChanged(tr("Extracting: %1").arg(QString::fromLatin1(unshield_file_name(unshield, index)))); diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index bdaf44198f..fb55bc88f8 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -11,31 +11,29 @@ #include "../inisettings.hpp" + namespace Wizard { + enum Component { + Component_Morrowind, + Component_Tribunal, + Component_Bloodmoon + }; + + //Q_DECLARE_METATYPE(Wizard::Component) + class UnshieldWorker : public QObject { Q_OBJECT + Q_ENUMS(Wizard::Component) public: UnshieldWorker(QObject *parent = 0); ~UnshieldWorker(); - void setInstallMorrowind(bool install); - void setInstallTribunal(bool install); - void setInstallBloodmoon(bool install); + void setInstallComponent(Wizard::Component component, bool install); - bool getInstallMorrowind(); - bool getInstallTribunal(); - bool getInstallBloodmoon(); - - void setMorrowindPath(const QString &path); - void setTribunalPath(const QString &path); - void setBloodmoonPath(const QString &path); - - QString getMorrowindPath(); - QString getTribunalPath(); - QString getBloodmoonPath(); + void setComponentPath(Wizard::Component component, const QString &path); void setPath(const QString &path); void setIniPath(const QString &path); @@ -47,13 +45,12 @@ namespace Wizard private: - void setMorrowindDone(bool done); - void setTribunalDone(bool done); - void setBloodmoonDone(bool done); + bool getInstallComponent(Component component); - bool getMorrowindDone(); - bool getTribunalDone(); - bool getBloodmoonDone(); + QString getComponentPath(Component component); + + void setComponentDone(Component component, bool done = true); + bool getComponentDone(Component component); bool removeDirectory(const QString &dirName); @@ -75,6 +72,9 @@ namespace Wizard bool installTribunal(); bool installBloodmoon(); + bool installComponent(Component component); + void setupAddon(Component component); + bool mInstallMorrowind; bool mInstallTribunal; bool mInstallBloodmoon; @@ -104,7 +104,7 @@ namespace Wizard signals: void finished(); - void requestFileDialog(const QString &component); + void requestFileDialog(Wizard::Component component); void textChanged(const QString &text); void logTextChanged(const QString &text); From 1262eab03ab51c7629a20f8c225c5c490d6ad7e3 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 28 Jan 2014 01:03:47 +0100 Subject: [PATCH 025/303] Added some nice error messages to the installation process --- apps/wizard/conclusionpage.cpp | 17 ++++++--- apps/wizard/installationpage.cpp | 48 +++++++++++++++++++------ apps/wizard/installationpage.hpp | 2 +- apps/wizard/mainwizard.hpp | 2 ++ apps/wizard/unshield/unshieldworker.cpp | 45 ++++++++++++++--------- apps/wizard/unshield/unshieldworker.hpp | 4 +-- files/ui/wizard/conclusionpage.ui | 6 ++++ files/ui/wizard/installationpage.ui | 4 +-- 8 files changed, 92 insertions(+), 36 deletions(-) diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index 55b03b9129..61d58eaba1 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -13,13 +13,20 @@ Wizard::ConclusionPage::ConclusionPage(MainWizard *wizard) : void Wizard::ConclusionPage::initializePage() { - if (field("installation.new").toBool() == true) + if (!mWizard->mError) { - textLabel->setText(tr("The OpenMW Wizard successfully installed Morrowind on your computer.\n\n") + - tr("Click Finish to close the Wizard.")); + if (field("installation.new").toBool() == true) + { + textLabel->setText(tr("

The OpenMW Wizard successfully installed Morrowind on your computer.

\ +

Click Finish to close the Wizard.

")); + } else { + textLabel->setText(tr("

The OpenMW Wizard successfully modified your existing Morrowind installation.

\ +

Click Finish to close the Wizard.

")); + } } else { - textLabel->setText(tr("The OpenMW Wizard successfully modified your existing Morrowind installation.\n\n") + - tr("Click Finish to close the Wizard.")); + textLabel->setText(tr("

The OpenMW Wizard failed to install Morrowind on your computer.

\ +

Please report any bugs you might have encountered to our \ + bug tracker.
Make sure to include the installation log.


")); } } diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 47d10c45f9..fc527ed3f6 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -23,8 +23,8 @@ void Wizard::InstallationPage::initializePage() QString path(field("installation.path").toString()); QStringList components(field("installation.components").toStringList()); - logTextEdit->append(QString("Installing to %1").arg(path)); - logTextEdit->append(QString("Installing %1.").arg(components.join(", "))); + logTextEdit->appendPlainText(QString("Installing to %1").arg(path)); + logTextEdit->appendPlainText(QString("Installing %1.").arg(components.join(", "))); installProgressBar->setMinimum(0); @@ -39,11 +39,11 @@ void Wizard::InstallationPage::initializePage() else { if (components.contains(QLatin1String("Tribunal")) - && mWizard->mInstallations[path]->hasTribunal == false) + && !mWizard->mInstallations[path]->hasTribunal) installProgressBar->setMaximum(100); if (components.contains(QLatin1String("Bloodmoon")) - && mWizard->mInstallations[path]->hasBloodmoon == false) + && !mWizard->mInstallations[path]->hasBloodmoon) installProgressBar->setMaximum(installProgressBar->maximum() + 100); } @@ -76,14 +76,14 @@ void Wizard::InstallationPage::startInstallation() connect(mUnshield, SIGNAL(finished()), this, SLOT(installationFinished()), Qt::QueuedConnection); - connect(mUnshield, SIGNAL(error(QString)), - this, SLOT(installationError(QString)), Qt::QueuedConnection); + connect(mUnshield, SIGNAL(error(QString, QString)), + this, SLOT(installationError(QString, QString)), Qt::QueuedConnection); connect(mUnshield, SIGNAL(textChanged(QString)), installProgressLabel, SLOT(setText(QString)), Qt::QueuedConnection); connect(mUnshield, SIGNAL(textChanged(QString)), - logTextEdit, SLOT(append(QString)), Qt::QueuedConnection); + logTextEdit, SLOT(appendPlainText(QString)), Qt::QueuedConnection); connect(mUnshield, SIGNAL(progressChanged(int)), installProgressBar, SLOT(setValue(int)), Qt::QueuedConnection); @@ -170,17 +170,45 @@ void Wizard::InstallationPage::installationFinished() } -void Wizard::InstallationPage::installationError(const QString &text) +void Wizard::InstallationPage::installationError(const QString &text, const QString &details) { qDebug() << "error: " << text; + + installProgressLabel->setText(tr("Installation failed!")); + + logTextEdit->appendHtml(tr("Error: %1").arg(text)); + logTextEdit->appendHtml(tr("%1").arg(details)); + + QMessageBox msgBox; + msgBox.setWindowTitle(tr("An error occurred")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

The Wizard has encountered an error

\ +

The error reported was:

%1

\ +

Press "Show Details..." for more information.

").arg(text)); + + msgBox.setDetailedText(details); + msgBox.exec(); + + mWizard->mError = true; + emit completeChanged(); + } bool Wizard::InstallationPage::isComplete() const { - return mFinished; + if (!mWizard->mError) { + return mFinished; + } else { + return true; + } } int Wizard::InstallationPage::nextId() const { - return MainWizard::Page_Import; + if (!mWizard->mError) { + return MainWizard::Page_Import; + } else { + return MainWizard::Page_Conclusion; + } } diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index 3989177abd..7001b26308 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -37,7 +37,7 @@ namespace Wizard void showFileDialog(Wizard::Component component); void installationFinished(); - void installationError(const QString &text); + void installationError(const QString &text, const QString &details); protected: void initializePage(); diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index 0d4c05fce2..31a93ffbbf 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -44,6 +44,8 @@ namespace Wizard Files::ConfigurationManager mCfgMgr; + bool mError; + private: void setupInstallations(); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 9c635818be..7becec1014 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -185,8 +185,8 @@ void Wizard::UnshieldWorker::setupSettings() QFile file(getIniPath()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - // TODO: Emit error signal qDebug() << "Error opening .ini file!"; + emit error(tr("Failed to open Morrowind configuration file!"), tr("Opening %1 failed: %2.").arg(getIniPath(), file.errorString())); return; } @@ -240,6 +240,7 @@ bool Wizard::UnshieldWorker::copyFile(const QString &source, const QString &dest } } else { qDebug() << "copy failed! " << file.errorString(); + emit error(tr("Failed to copy file!"), tr("Copying %1 to %2 failed: %3.").arg(source, destination, file.errorString())); } return false; @@ -274,7 +275,6 @@ bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString if (info.isDir()) { result = moveDirectory(info.absoluteFilePath(), destDir.absolutePath() + relativePath); } else { -// qDebug() << "moving: " << info.absoluteFilePath() << " to: " << destDir.absolutePath() + relativePath; result = moveFile(info.absoluteFilePath(), destDir.absolutePath() + relativePath); } } @@ -363,6 +363,7 @@ void Wizard::UnshieldWorker::extract() setComponentDone(Wizard::Component_Morrowind, true); } else { qDebug() << "Erorr installing Morrowind"; + return; } } @@ -414,8 +415,10 @@ void Wizard::UnshieldWorker::setupAddon(Component component) if (component == Wizard::Component_Bloodmoon) name = QLatin1String("Bloodmoon"); - if (name.isEmpty()) - return; // Not a valid addon + if (name.isEmpty()) { + emit error(tr("Component parameter is invalid!"), tr("An invalid component parameter was supplied.")); + return; + } if (!disk.cd(name)) { qDebug() << "not found on cd!"; @@ -474,8 +477,10 @@ bool Wizard::UnshieldWorker::installComponent(Component component) break; } - if (name.isEmpty()) + if (name.isEmpty()) { + emit error(tr("Component parameter is invalid!"), tr("An invalid component parameter was supplied.")); return false; + } emit textChanged(tr("Installing %0").arg(name)); @@ -483,6 +488,7 @@ bool Wizard::UnshieldWorker::installComponent(Component component) if (!disk.exists()) { qDebug() << "Component path not set: " << getComponentPath(Wizard::Component_Morrowind); + emit error(tr("Component path not set!"), tr("The source path for %0 was not set.").arg(name)); return false; } @@ -496,29 +502,34 @@ bool Wizard::UnshieldWorker::installComponent(Component component) if (!temp.mkpath(tempPath)) { qDebug() << "Can't make path"; + emit error(tr("Cannot create temporary directory!"), tr("Failed to create %0.").arg(tempPath)); return false; } temp.setPath(tempPath); if (!temp.mkdir(name)) { - qDebug() << "Can't make dir"; + emit error(tr("Cannot create temporary directory!"), tr("Failed to create %0.").arg(temp.absoluteFilePath(name))); return false; } if (!temp.cd(name)) { qDebug() << "Can't cd to dir"; + emit error(tr("Cannot move into temporary directory!"), tr("Failed to move into %0.").arg(temp.absoluteFilePath(name))); return false; } // Extract the installation files - extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath()); + if (!extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath())) + return false; - // TODO: Throw error; // Move the files from the temporary path to the destination folder emit textChanged(tr("Moving installation files")); if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { qDebug() << "failed to move files!"; + emit error(tr("Moving extracted files failed!"), + tr("Failed to move files from %0 to %1.").arg(temp.absoluteFilePath(QLatin1String("Data Files")), + getPath())); return false; } @@ -538,6 +549,7 @@ bool Wizard::UnshieldWorker::installComponent(Component component) moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); } else { qDebug() << "Could not find ini file!"; + emit error(tr("Could not find Morrowind configuration file!"), tr("Failed to find %0.").arg(iniPath)); return false; } } @@ -599,7 +611,7 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp success = unshield_file_save(unshield, index, fileName.toLatin1().constData()); if (!success) { - emit error(tr("Failed to extract %1").arg(fileName)); + emit error(tr("Failed to extract %1.").arg(QString::fromLatin1(unshield_file_name(unshield, index))), tr("Complete path: %1.").arg(fileName)); dir.remove(fileName); } @@ -611,9 +623,8 @@ bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fil Unshield *unshield; unshield = unshield_open(cabFile.toLatin1().constData()); - // TODO: Proper error if (!unshield) { - emit error(tr("Failed to open %1").arg(cabFile)); + emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); return false; } @@ -635,15 +646,16 @@ bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fil return false; } -void Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &outputDir) +bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &outputDir) { + bool success; + Unshield *unshield; unshield = unshield_open(cabFile.toLatin1().constData()); - // TODO: Proper error if (!unshield) { - emit error(tr("Failed to open %1").arg(cabFile)); - return; + emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); + return false; } int counter = 0; @@ -655,11 +667,12 @@ void Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &o for (size_t j=group->first_file; j<=group->last_file; ++j) { if (unshield_file_is_valid(unshield, j)) { - extractFile(unshield, outputDir, group->name, j, counter); + success = extractFile(unshield, outputDir, group->name, j, counter); ++counter; } } } unshield_close(unshield); + return success; } diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index fb55bc88f8..501daaf308 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -62,7 +62,7 @@ namespace Wizard void setupSettings(); - void extractCab(const QString &cabFile, const QString &outputDir); + bool extractCab(const QString &cabFile, const QString &outputDir); bool extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter); bool findFile(const QString &cabFile, const QString &fileName); @@ -109,7 +109,7 @@ namespace Wizard void textChanged(const QString &text); void logTextChanged(const QString &text); - void error(const QString &text); + void error(const QString &text, const QString &details); void progressChanged(int progress); diff --git a/files/ui/wizard/conclusionpage.ui b/files/ui/wizard/conclusionpage.ui index a27f667899..eb6828b387 100644 --- a/files/ui/wizard/conclusionpage.ui +++ b/files/ui/wizard/conclusionpage.ui @@ -22,9 +22,15 @@ Placeholder + + Qt::RichText + true + + true +
diff --git a/files/ui/wizard/installationpage.ui b/files/ui/wizard/installationpage.ui index 664e105560..6877f1e582 100644 --- a/files/ui/wizard/installationpage.ui +++ b/files/ui/wizard/installationpage.ui @@ -23,7 +23,7 @@ - Extracting: %1 + @@ -35,7 +35,7 @@
- + false From 2ccf4d61124d0e62826e62252648fa3195792b80 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 28 Jan 2014 11:51:13 +0100 Subject: [PATCH 026/303] Post-merge cleanups, using confignmanager for XDG paths now, thanks scrawl! --- apps/wizard/installationtargetpage.cpp | 28 +++++--------------------- apps/wizard/installationtargetpage.hpp | 8 +++++++- apps/wizard/mainwizard.cpp | 6 +++--- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index 60acfd5f78..459e7145de 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -2,13 +2,13 @@ #include #include -#include #include "mainwizard.hpp" -Wizard::InstallationTargetPage::InstallationTargetPage(MainWizard *wizard) : +Wizard::InstallationTargetPage::InstallationTargetPage(MainWizard *wizard, const Files::ConfigurationManager &cfg) : QWizardPage(wizard), - mWizard(wizard) + mWizard(wizard), + mCfgMgr(cfg) { setupUi(this); @@ -17,24 +17,8 @@ Wizard::InstallationTargetPage::InstallationTargetPage(MainWizard *wizard) : void Wizard::InstallationTargetPage::initializePage() { - qDebug() << mWizard->field("installation.language"); - -#ifdef Q_OS_WIN - QString path = QDesktopServices::storageLocation(QDesktopServices::DocumentsLocation); -#endif - -#ifdef Q_OS_MAC - QString path = QDesktopServices::storageLocation(QDesktopServices::DataLocation); -#endif - -#if defined(Q_OS_LINUX) || defined(Q_OS_UNIX) - QString path = QFile::decodeName(qgetenv("XDG_DATA_HOME")); - - if (path.isEmpty()) - path = QDir::homePath() + QLatin1String("/.local/share"); -#endif - - path.append(QLatin1String("/openmw/data")); + QString path(QFile::decodeName(mCfgMgr.getUserDataPath().string().c_str())); + path.append(QDir::separator() + QLatin1String("data")); if (!QFile::exists(path)) { QDir dir; @@ -43,8 +27,6 @@ void Wizard::InstallationTargetPage::initializePage() QDir dir(path); targetLineEdit->setText(QDir::toNativeSeparators(dir.absolutePath())); - - qDebug() << path; } void Wizard::InstallationTargetPage::on_browseButton_clicked() diff --git a/apps/wizard/installationtargetpage.hpp b/apps/wizard/installationtargetpage.hpp index ccaac5969d..f6d28b0130 100644 --- a/apps/wizard/installationtargetpage.hpp +++ b/apps/wizard/installationtargetpage.hpp @@ -5,6 +5,11 @@ #include "ui_installationtargetpage.h" +namespace Files +{ + struct ConfigurationManager; +} + namespace Wizard { class MainWizard; @@ -13,7 +18,7 @@ namespace Wizard { Q_OBJECT public: - InstallationTargetPage(MainWizard *wizard); + InstallationTargetPage(MainWizard *wizard, const Files::ConfigurationManager &cfg); int nextId() const; @@ -22,6 +27,7 @@ namespace Wizard private: MainWizard *mWizard; + const Files::ConfigurationManager &mCfgMgr; protected: void initializePage(); diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index cbc9253edc..aef59fa4ed 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -41,7 +41,7 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : void Wizard::MainWizard::setupInstallations() { - QString userPath(QFile::decodeName(mCfgMgr.getUserPath().string().c_str())); + QString userPath(QFile::decodeName(mCfgMgr.getUserConfigPath().string().c_str())); QString globalPath(QFile::decodeName(mCfgMgr.getGlobalPath().string().c_str())); QStringList paths; @@ -122,7 +122,7 @@ void Wizard::MainWizard::setupPages() setPage(Page_MethodSelection, new MethodSelectionPage(this)); setPage(Page_LanguageSelection, new LanguageSelectionPage(this)); setPage(Page_ExistingInstallation, new ExistingInstallationPage(this)); - setPage(Page_InstallationTarget, new InstallationTargetPage(this)); + setPage(Page_InstallationTarget, new InstallationTargetPage(this, mCfgMgr)); setPage(Page_ComponentSelection, new ComponentSelectionPage(this)); setPage(Page_Installation, new InstallationPage(this)); setPage(Page_Import, new ImportPage(this)); @@ -137,7 +137,7 @@ void Wizard::MainWizard::accept() void Wizard::MainWizard::writeSettings() { - QString userPath(QFile::decodeName(mCfgMgr.getUserPath().string().c_str())); + QString userPath(QFile::decodeName(mCfgMgr.getUserConfigPath().string().c_str())); QFile file(userPath + QLatin1String("openmw.cfg")); if (!file.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) { From 5024f1bf7763ccaf569c483b8e1a6a3c36029e65 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 28 Jan 2014 13:27:09 +0100 Subject: [PATCH 027/303] Added a warning when trying to install tribunal after bloodmoon --- apps/wizard/componentselectionpage.cpp | 61 ++++++++++++++++++++--- apps/wizard/componentselectionpage.hpp | 1 + apps/wizard/existinginstallationpage.cpp | 6 +-- apps/wizard/installationpage.cpp | 16 +++++- apps/wizard/installationtargetpage.cpp | 4 +- apps/wizard/languageselectionpage.cpp | 5 +- apps/wizard/mainwizard.cpp | 17 +++---- apps/wizard/methodselectionpage.cpp | 6 ++- apps/wizard/utils/componentlistwidget.cpp | 18 ++++--- apps/wizard/utils/componentlistwidget.hpp | 3 +- 10 files changed, 99 insertions(+), 38 deletions(-) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index 7d934fe849..87779e93dd 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -1,7 +1,9 @@ #include "componentselectionpage.hpp" #include +#include #include +#include #include "mainwizard.hpp" @@ -51,7 +53,7 @@ void Wizard::ComponentSelectionPage::initializePage() { componentsList->clear(); - QString path = field("installation.path").toString(); + QString path(field("installation.path").toString()); QListWidgetItem *morrowindItem = new QListWidgetItem(QLatin1String("Morrowind")); QListWidgetItem *tribunalItem = new QListWidgetItem(QLatin1String("Tribunal")); @@ -72,7 +74,7 @@ void Wizard::ComponentSelectionPage::initializePage() componentsList->addItem(bloodmoonItem); } else { - if (mWizard->mInstallations[path]->hasMorrowind == true) { + if (mWizard->mInstallations[path]->hasMorrowind) { morrowindItem->setText(tr("Morrowind\t\t(installed)")); morrowindItem->setFlags(morrowindItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); morrowindItem->setData(Qt::CheckStateRole, Qt::Unchecked); @@ -83,7 +85,7 @@ void Wizard::ComponentSelectionPage::initializePage() componentsList->addItem(morrowindItem); - if (mWizard->mInstallations[path]->hasTribunal == true) { + if (mWizard->mInstallations[path]->hasTribunal) { tribunalItem->setText(tr("Tribunal\t\t(installed)")); tribunalItem->setFlags(tribunalItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); tribunalItem->setData(Qt::CheckStateRole, Qt::Unchecked); @@ -94,7 +96,7 @@ void Wizard::ComponentSelectionPage::initializePage() componentsList->addItem(tribunalItem); - if (mWizard->mInstallations[path]->hasBloodmoon == true) { + if (mWizard->mInstallations[path]->hasBloodmoon) { bloodmoonItem->setText(tr("Bloodmoon\t\t(installed)")); bloodmoonItem->setFlags(bloodmoonItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); bloodmoonItem->setData(Qt::CheckStateRole, Qt::Unchecked); @@ -107,10 +109,55 @@ void Wizard::ComponentSelectionPage::initializePage() } } +bool Wizard::ComponentSelectionPage::validatePage() +{ + QStringList components(field("installation.components").toStringList()); + QString path(field("installation.path").toString()); + + qDebug() << components << path << mWizard->mInstallations[path]; + + if (field("installation.new").toBool() == false) { + if (components.contains(QLatin1String("Tribunal")) && !components.contains(QLatin1String("Bloodmoon"))) + { + if (mWizard->mInstallations[path]->hasBloodmoon) + { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("About to install Tribunal after Bloodmoon")); + msgBox.setIcon(QMessageBox::Information); + msgBox.setStandardButtons(QMessageBox::Cancel); + msgBox.setText(tr("

You are about to install Tribunal

\ +

Bloodmoon is already installed on your computer.

\ +

However, it is recommended that you install Tribunal before Bloodmoon.

\ +

Would you like to re-install Bloodmoon?

")); + + QAbstractButton *reinstallButton = msgBox.addButton(tr("Re-install &Bloodmoon"), QMessageBox::ActionRole); + msgBox.exec(); + + + if (msgBox.clickedButton() == reinstallButton) { + // Force reinstallation + mWizard->mInstallations[path]->hasBloodmoon = false; + QList items = componentsList->findItems(QLatin1String("Bloodmoon"), Qt::MatchStartsWith); + + foreach (QListWidgetItem *item, items) { + item->setText(QLatin1String("Bloodmoon")); + item->setCheckState(Qt::Checked); + } + + return true; + } + } + } + } + + return true; +} + int Wizard::ComponentSelectionPage::nextId() const { - if (isCommitPage()) + if (isCommitPage()) { return MainWizard::Page_Installation; - - return MainWizard::Page_Import; + } else { + return MainWizard::Page_Import; + } } diff --git a/apps/wizard/componentselectionpage.hpp b/apps/wizard/componentselectionpage.hpp index 8b4c186d09..ed007fd087 100644 --- a/apps/wizard/componentselectionpage.hpp +++ b/apps/wizard/componentselectionpage.hpp @@ -16,6 +16,7 @@ namespace Wizard ComponentSelectionPage(MainWizard *wizard); int nextId() const; + virtual bool validatePage(); private slots: void updateButton(QListWidgetItem *item); diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 5c66ceb4f5..c1dbf8e9bf 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -57,7 +57,7 @@ void Wizard::ExistingInstallationPage::textChanged(const QString &text) // Set the installation path manually, as registerField doesn't work // Because it doesn't accept two widgets operating on a single field if (!text.isEmpty()) - mWizard->setField("installation.path", text); + mWizard->setField(QLatin1String("installation.path"), text); } void Wizard::ExistingInstallationPage::initializePage() @@ -135,7 +135,7 @@ int Wizard::ExistingInstallationPage::nextId() const QString path(field("installation.path").toString()); if (path.isEmpty()) - return MainWizard::Page_ComponentSelection; + return MainWizard::Page_LanguageSelection; if (mWizard->mInstallations[path]->hasMorrowind == true && mWizard->mInstallations[path]->hasTribunal == true && @@ -143,6 +143,6 @@ int Wizard::ExistingInstallationPage::nextId() const { return MainWizard::Page_Import; } else { - return MainWizard::Page_ComponentSelection; + return MainWizard::Page_LanguageSelection; } } diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index fc527ed3f6..e838a19c12 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -138,9 +138,23 @@ void Wizard::InstallationPage::startInstallation() void Wizard::InstallationPage::showFileDialog(Wizard::Component component) { + QString name; + switch (component) { + + case Wizard::Component_Morrowind: + name = QLatin1String("Morrowind"); + break; + case Wizard::Component_Tribunal: + name = QLatin1String("Tribunal"); + break; + case Wizard::Component_Bloodmoon: + name = QLatin1String("Bloodmoon"); + break; + } + QString fileName = QFileDialog::getOpenFileName( this, - tr("Select installation file"), + tr("Select %1 installation file").arg(name), QDir::rootPath(), tr("InstallShield header files (*.hdr)")); diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index 459e7145de..900a978cf8 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -34,7 +34,7 @@ void Wizard::InstallationTargetPage::on_browseButton_clicked() QString selectedPath = QFileDialog::getExistingDirectory( this, tr("Select where to install Morrowind"), - QDir::currentPath(), + QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); qDebug() << selectedPath; @@ -49,5 +49,5 @@ void Wizard::InstallationTargetPage::on_browseButton_clicked() int Wizard::InstallationTargetPage::nextId() const { - return MainWizard::Page_ComponentSelection; + return MainWizard::Page_LanguageSelection; } diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp index 102a4c39a5..b921d5ddbc 100644 --- a/apps/wizard/languageselectionpage.cpp +++ b/apps/wizard/languageselectionpage.cpp @@ -27,8 +27,5 @@ void Wizard::LanguageSelectionPage::initializePage() int Wizard::LanguageSelectionPage::nextId() const { - if (field("installation.new").toBool() == true) - return MainWizard::Page_InstallationTarget; - - return MainWizard::Page_ExistingInstallation; + return MainWizard::Page_ComponentSelection; } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index aef59fa4ed..0913cf9e4d 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -73,14 +73,10 @@ void Wizard::MainWizard::setupInstallations() file.close(); } - // Check if the paths actually contain data files + // Check if the paths actually contains a Morrowind installation foreach (const QString path, mGameSettings.getDataDirs()) { - QDir dir(path); - QStringList filters; - filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; - // Add to Wizard installations - if (!dir.entryList(filters).isEmpty()) + if (findFiles(QLatin1String("Morrowind"), path)) addInstallation(path); } @@ -112,8 +108,10 @@ void Wizard::MainWizard::addInstallation(const QString &path) mInstallations.insert(QDir::toNativeSeparators(path), install); // Add it to the openmw.cfg too - mGameSettings.setMultiValue(QString("data"), path); - mGameSettings.addDataDir(path); + if (!mGameSettings.getDataDirs().contains(path)) { + mGameSettings.setMultiValue(QString("data"), path); + mGameSettings.addDataDir(path); + } } void Wizard::MainWizard::setupPages() @@ -168,8 +166,7 @@ bool Wizard::MainWizard::findFiles(const QString &name, const QString &path) return false; // TODO: add MIME handling to make sure the files are real - if (dir.exists(name + QLatin1String(".esm")) && dir.exists(name + QLatin1String(".bsa"))) - { + if (dir.exists(name + QLatin1String(".esm")) && dir.exists(name + QLatin1String(".bsa"))) { return true; } else { return false; diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp index 4185fa2bcb..6db0779a6a 100644 --- a/apps/wizard/methodselectionpage.cpp +++ b/apps/wizard/methodselectionpage.cpp @@ -13,5 +13,9 @@ Wizard::MethodSelectionPage::MethodSelectionPage(MainWizard *wizard) : int Wizard::MethodSelectionPage::nextId() const { - return MainWizard::Page_LanguageSelection; + if (field("installation.new").toBool() == true) { + return MainWizard::Page_InstallationTarget; + } else { + return MainWizard::Page_ExistingInstallation; + } } diff --git a/apps/wizard/utils/componentlistwidget.cpp b/apps/wizard/utils/componentlistwidget.cpp index c9a1c19400..6a5d019b5e 100644 --- a/apps/wizard/utils/componentlistwidget.cpp +++ b/apps/wizard/utils/componentlistwidget.cpp @@ -10,15 +10,9 @@ ComponentListWidget::ComponentListWidget(QWidget *parent) : connect(this, SIGNAL(itemChanged(QListWidgetItem *)), this, SLOT(updateCheckedItems(QListWidgetItem *))); -} - -void ComponentListWidget::addItem(QListWidgetItem *item) -{ - // The model does not emit a dataChanged signal when items are added - // So we need to update manually - QListWidget::insertItem(count(), item); - updateCheckedItems(item); + connect(model(), SIGNAL(rowsInserted(QModelIndex, int, int)), + this, SLOT(updateCheckedItems(QModelIndex, int, int))); } QStringList ComponentListWidget::checkedItems() @@ -27,8 +21,16 @@ QStringList ComponentListWidget::checkedItems() return mCheckedItems; } +void ComponentListWidget::updateCheckedItems(const QModelIndex &index, int start, int end) +{ + updateCheckedItems(item(start)); +} + void ComponentListWidget::updateCheckedItems(QListWidgetItem *item) { + if (!item) + return; + QString text = item->text(); if (item->checkState() == Qt::Checked) { diff --git a/apps/wizard/utils/componentlistwidget.hpp b/apps/wizard/utils/componentlistwidget.hpp index 6869629d34..23965f8a6b 100644 --- a/apps/wizard/utils/componentlistwidget.hpp +++ b/apps/wizard/utils/componentlistwidget.hpp @@ -15,13 +15,12 @@ public: QStringList mCheckedItems; QStringList checkedItems(); - void addItem(QListWidgetItem *item); - signals: void checkedItemsChanged(const QStringList &items); private slots: void updateCheckedItems(QListWidgetItem *item); + void updateCheckedItems(const QModelIndex &index, int start, int end); }; #endif // COMPONENTLISTWIDGET_HPP From ceb66d0790ed8aa1c62b6ca2564db311a2921d38 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 28 Jan 2014 13:36:14 +0100 Subject: [PATCH 028/303] Also install the separate Sounds directory for Tribunal --- apps/wizard/unshield/unshieldworker.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 7becec1014..d7cda05a0e 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -554,6 +554,17 @@ bool Wizard::UnshieldWorker::installComponent(Component component) } } + if (component == Wizard::Component_Tribunal) + { + QFileInfo sounds(temp.absoluteFilePath(QLatin1String("Sounds"))); + + if (sounds.exists()) { + emit textChanged(tr("Extracting: Sound directory")); + copyDirectory(sounds.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Sound")); + } + + } + if (component == Wizard::Component_Bloodmoon) { QFileInfo patch(temp.absoluteFilePath(QLatin1String("Tribunal Patch") + QDir::separator() + QLatin1String("Tribunal.esm"))); From e8170adde5b02fe1a66a471831a8d117f6f4b277 Mon Sep 17 00:00:00 2001 From: pvdk Date: Sat, 8 Feb 2014 00:09:25 +0100 Subject: [PATCH 029/303] Some minor changes, implemented suggestions --- apps/wizard/installationpage.cpp | 15 ++++----------- apps/wizard/installationtargetpage.cpp | 22 ++++++++++++++++++++++ apps/wizard/installationtargetpage.hpp | 1 + apps/wizard/unshield/unshieldworker.cpp | 2 ++ apps/wizard/unshield/unshieldworker.hpp | 2 -- files/ui/wizard/importpage.ui | 6 +++--- files/ui/wizard/methodselectionpage.ui | 12 +++++------- 7 files changed, 37 insertions(+), 23 deletions(-) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index e838a19c12..efa6880be1 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -32,12 +32,9 @@ void Wizard::InstallationPage::initializePage() // That way installing all three components would yield 300% // When one component is done the bar will be filled by 33% - if (field("installation.new").toBool() == true) - { + if (field("installation.new").toBool() == true) { installProgressBar->setMaximum((components.count() * 100)); - } - else - { + } else { if (components.contains(QLatin1String("Tribunal")) && !mWizard->mInstallations[path]->hasTribunal) installProgressBar->setMaximum(100); @@ -59,8 +56,6 @@ void Wizard::InstallationPage::startInstallation() mUnshield = new UnshieldWorker(); mUnshield->moveToThread(thread); - qRegisterMetaType("Wizard::Component"); - connect(thread, SIGNAL(started()), mUnshield, SLOT(extract())); @@ -125,11 +120,9 @@ void Wizard::InstallationPage::startInstallation() if (language == QLatin1String("Polish")) { mUnshield->setIniCodec(QTextCodec::codecForName("windows-1250")); - } - else if (language == QLatin1String("Russian")) { + } else if (language == QLatin1String("Russian")) { mUnshield->setIniCodec(QTextCodec::codecForName("windows-1251")); - } - else { + } else { mUnshield->setIniCodec(QTextCodec::codecForName("windows-1252")); } diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index 900a978cf8..306e7221c7 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "mainwizard.hpp" @@ -29,6 +30,27 @@ void Wizard::InstallationTargetPage::initializePage() targetLineEdit->setText(QDir::toNativeSeparators(dir.absolutePath())); } +bool Wizard::InstallationTargetPage::validatePage() +{ + QString path(field("installation.path").toString()); + + qDebug() << "Validating path: " << path; + + if (mWizard->findFiles(QLatin1String("Morrowind"), path)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Destination not empty")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

The destination directory is not empty

\ +

An existing Morrowind installation is present in the specified location.

\ +

Please specify a different location, or go back and select the location as an existing installation.

")); + msgBox.exec(); + return false; + } + + return true; +} + void Wizard::InstallationTargetPage::on_browseButton_clicked() { QString selectedPath = QFileDialog::getExistingDirectory( diff --git a/apps/wizard/installationtargetpage.hpp b/apps/wizard/installationtargetpage.hpp index f6d28b0130..0711c1c050 100644 --- a/apps/wizard/installationtargetpage.hpp +++ b/apps/wizard/installationtargetpage.hpp @@ -21,6 +21,7 @@ namespace Wizard InstallationTargetPage(MainWizard *wizard, const Files::ConfigurationManager &cfg); int nextId() const; + virtual bool validatePage(); private slots: void on_browseButton_clicked(); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index d7cda05a0e..02e50a7e33 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -37,6 +37,8 @@ Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : mMorrowindDone = false; mTribunalDone = false; mBloodmoonDone = false; + + qRegisterMetaType("Wizard::Component"); } Wizard::UnshieldWorker::~UnshieldWorker() diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index 501daaf308..d110851f4f 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -20,8 +20,6 @@ namespace Wizard Component_Bloodmoon }; - //Q_DECLARE_METATYPE(Wizard::Component) - class UnshieldWorker : public QObject { Q_OBJECT diff --git a/files/ui/wizard/importpage.ui b/files/ui/wizard/importpage.ui index 21037f3e8b..5b078efca7 100644 --- a/files/ui/wizard/importpage.ui +++ b/files/ui/wizard/importpage.ui @@ -6,8 +6,8 @@ 0 0 - 513 - 328 + 511 + 326 @@ -43,7 +43,7 @@ - Import previously selected add-ons (creates a new Content List in the launcher) + Import add-on and plugin selection (creates a new Content List in the launcher) diff --git a/files/ui/wizard/methodselectionpage.ui b/files/ui/wizard/methodselectionpage.ui index e77349f5cc..122ef754e5 100644 --- a/files/ui/wizard/methodselectionpage.ui +++ b/files/ui/wizard/methodselectionpage.ui @@ -6,8 +6,8 @@ 0 0 - 398 - 298 + 396 + 296 @@ -26,7 +26,7 @@ font-weight:bold; - Install to a new location + Install Morrowind to a new location true @@ -94,7 +94,7 @@ font-weight:bold - Select an existing installation + Select an existing Morrowind installation
@@ -149,8 +149,6 @@
- - - + From 6916c0bc9479a65cadbe384be6ff3bde72c2ce70 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 18 Feb 2014 11:55:26 +0100 Subject: [PATCH 030/303] Wizard now writes valid Morrowind.ini files --- apps/wizard/existinginstallationpage.cpp | 3 +- apps/wizard/inisettings.cpp | 180 ++++++++++++++++++++++- apps/wizard/inisettings.hpp | 13 +- apps/wizard/installationpage.cpp | 1 + apps/wizard/installationtargetpage.cpp | 12 +- apps/wizard/mainwizard.cpp | 1 + apps/wizard/unshield/unshieldworker.cpp | 66 ++++++++- apps/wizard/unshield/unshieldworker.hpp | 6 +- 8 files changed, 262 insertions(+), 20 deletions(-) diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index c1dbf8e9bf..34e771e3d7 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -38,8 +38,7 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() QString path(QDir::toNativeSeparators(info.absolutePath())); QList items = installationsList->findItems(path, Qt::MatchExactly); - if (items.isEmpty()) - { + if (items.isEmpty()) { // Path is not yet in the list, add it mWizard->addInstallation(path); diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp index 2c099acecc..f34699742b 100644 --- a/apps/wizard/inisettings.cpp +++ b/apps/wizard/inisettings.cpp @@ -1,8 +1,10 @@ #include "inisettings.hpp" +#include #include #include +#include #include #include #include @@ -15,25 +17,40 @@ Wizard::IniSettings::~IniSettings() { } +QStringList Wizard::IniSettings::findKeys(const QString &text) +{ + QStringList result; + + foreach (const QString &key, mSettings.keys()) { + + if (key.startsWith(text)) + result << key; + + } + + return result; +} + bool Wizard::IniSettings::readFile(QTextStream &stream) { + qDebug() << "readFile called!"; // Look for a square bracket, "'\\[" // that has one or more "not nothing" in it, "([^]]+)" // and is closed with a square bracket, "\\]" - QRegExp sectionRe("^\\[([^]]+)\\]"); + QRegExp sectionRe(QLatin1String("^\\[([^]]+)\\]")); // Find any character(s) that is/are not equal sign(s), "[^=]+" // followed by an optional whitespace, an equal sign, and another optional whitespace, "\\s*=\\s*" // and one or more periods, "(.+)" - QRegExp keyRe("^([^=]+)\\s*=\\s*(.+)$"); + QRegExp keyRe(QLatin1String("^([^=]+)\\s*=\\s*(.+)$")); QString currentSection; while (!stream.atEnd()) { - QString line(stream.readLine()); + const QString line(stream.readLine()); - if (line.isEmpty() || line.startsWith(";")) + if (line.isEmpty() || line.startsWith(QLatin1Char(';'))) continue; if (sectionRe.exactMatch(line)) @@ -49,6 +66,7 @@ bool Wizard::IniSettings::readFile(QTextStream &stream) if (!currentSection.isEmpty()) key = currentSection + QLatin1Char('/') + key; + qDebug() << "adding: " << key << value; mSettings[key] = QVariant(value); } } @@ -56,13 +74,161 @@ bool Wizard::IniSettings::readFile(QTextStream &stream) return true; } -bool Wizard::IniSettings::writeFile(QTextStream &stream) +bool Wizard::IniSettings::writeFile(const QString &path, QTextStream &stream) { - qDebug() << "test! " << stream.readAll(); + // Look for a square bracket, "'\\[" + // that has one or more "not nothing" in it, "([^]]+)" + // and is closed with a square bracket, "\\]" + QRegExp sectionRe(QLatin1String("^\\[([^]]+)\\]")); + + // Find any character(s) that is/are not equal sign(s), "[^=]+" + // followed by an optional whitespace, an equal sign, and another optional whitespace, "\\s*=\\s*" + // and one or more periods, "(.+)" + QRegExp keyRe(QLatin1String("^([^=]+)\\s*=\\s*(.+)$")); + + const QStringList keys(mSettings.keys()); + + QString currentSection; + QString buffer; + + qDebug() << "Keys! " << keys; while (!stream.atEnd()) { - qDebug() << "test! " << stream.readLine(); + + const QString line(stream.readLine()); + + if (line.isEmpty() || line.startsWith(QLatin1Char(';'))) { + buffer.append(line + QLatin1String("\n")); + continue; + } + + if (sectionRe.exactMatch(line)) { + buffer.append(line + QLatin1String("\n")); + currentSection = sectionRe.cap(1); + } else if (keyRe.indexIn(line) != -1) { + QString key(keyRe.cap(1).trimmed()); + QString lookupKey(key); + + // Append the section, but only if there is one + if (!currentSection.isEmpty()) + lookupKey = currentSection + QLatin1Char('/') + key; + + buffer.append(key + QLatin1Char('=') + mSettings[lookupKey].toString() + QLatin1String("\n")); + mSettings.remove(lookupKey); + } } + // Add the new settings to the buffer + QHashIterator i(mSettings); + while (i.hasNext()) { + i.next(); + + QStringList fullKey(i.key().split(QLatin1Char('/'))); + QString section(fullKey.at(0)); + section.prepend(QLatin1Char('[')); + section.append(QLatin1Char(']')); + QString key(fullKey.at(1)); + + int index = buffer.lastIndexOf(section); + if (index != -1) { + // Append the new keys to the bottom of the section + index = buffer.indexOf(QLatin1Char('['), index + 1); + + if (index == -1 ) + { + // Beginning of next section not found, we are at the last section + if (buffer.lastIndexOf(QLatin1String("\n")) > (buffer.lastIndexOf(section) + section.length())) { + // There is a newline after the section + index = buffer.lastIndexOf(QLatin1String("\n")) - 1; + buffer.insert(index - 2, QString("\n%1=%2").arg(key, i.value().toString())); + mSettings.remove(i.key()); + continue; + } else { + // No newline found, or the last newline is before the last section + // Append the key to the bottom of the file + buffer.append(QString("\n%1=%2").arg(key, i.value().toString())); + mSettings.remove(i.key()); + continue; + } + } + + // Add the key at the index + buffer.insert(index - 1, QString("\n%1=%2").arg(key, i.value().toString())); + mSettings.remove(i.key()); + } else { + // Add the section to the end of the file, because it's not found + buffer.append(QString("\n%1\n").arg(section)); + i.previous(); + } + } + + // Now we reopen the file, this time we write + QFile file(path); + + if (file.open(QIODevice::ReadWrite | QIODevice::Truncate | QIODevice::Text)) { + QTextStream in(&file); + in.setCodec(stream.codec()); + + // Write the updated buffer to an empty file + in << buffer; + file.flush(); + file.close(); + } else { + return false; + } + + return true; +} + +bool Wizard::IniSettings::parseInx(const QString &path) +{ + QFile file(path); + + if (file.open(QIODevice::ReadOnly)) + { + + const QByteArray data(file.readAll()); + const QByteArray pattern("\x21\x00\x1A\x01\x04\x00\x04\x97\xFF\x06", 10); + + int i = 0; + while ((i = data.indexOf(pattern, i)) != -1) { + + int next = data.indexOf(pattern, i + 1); + if (next == -1) + break; + + QByteArray array(data.mid(i, (next - i))); + + // Skip some invalid entries + if (array.contains("\x04\x96\xFF")) { + ++i; + continue; + } + + // Remove the pattern from the beginning + array.remove(0, 12); + + int index = array.indexOf("\x06"); + const QString section(array.left(index)); + + // Figure how many characters to read for the key + int lenght = array.indexOf("\x06", section.length() + 3) - (section.length() + 3); + const QString key(array.mid(section.length() + 3, lenght)); + + QString value(array.mid(section.length() + key.length() + 6)); + //qDebug() << section << key << value; + + // Add the value + setValue(section + QLatin1Char('/') + key, QVariant(value)); + + ++i; + } + + file.close(); + } else { + qDebug() << "Failed to open INX file: " << path; + return false; + } + return true; } diff --git a/apps/wizard/inisettings.hpp b/apps/wizard/inisettings.hpp index 66fe96ec1f..d425a9b1b3 100644 --- a/apps/wizard/inisettings.hpp +++ b/apps/wizard/inisettings.hpp @@ -22,6 +22,11 @@ namespace Wizard return mSettings.value(key, defaultValue); } + inline QList values() const + { + return mSettings.values(); + } + inline void setValue(const QString &key, const QVariant &value) { mSettings.insert(key, value); @@ -32,11 +37,17 @@ namespace Wizard mSettings.remove(key); } + QStringList findKeys(const QString &text); + bool readFile(QTextStream &stream); - bool writeFile(QTextStream &stream); + bool writeFile(const QString &path, QTextStream &stream); + + bool parseInx(const QString &path); private: + int getLastNewline(const QString &buffer, int from); + SettingsMap mSettings; }; diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index efa6880be1..05d1b4af84 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -110,6 +110,7 @@ void Wizard::InstallationPage::startInstallation() // Set the location of the Morrowind.ini to update mUnshield->setIniPath(mWizard->mInstallations[path]->iniPath); + mUnshield->setupSettings(); } // Set the installation target path diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index 306e7221c7..daf2c08467 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -21,11 +21,6 @@ void Wizard::InstallationTargetPage::initializePage() QString path(QFile::decodeName(mCfgMgr.getUserDataPath().string().c_str())); path.append(QDir::separator() + QLatin1String("data")); - if (!QFile::exists(path)) { - QDir dir; - dir.mkpath(path); - } - QDir dir(path); targetLineEdit->setText(QDir::toNativeSeparators(dir.absolutePath())); } @@ -36,6 +31,13 @@ bool Wizard::InstallationTargetPage::validatePage() qDebug() << "Validating path: " << path; + // TODO: Check writeability + if (!QFile::exists(path)) { + QDir dir; + dir.mkpath(path); + return true; + } + if (mWizard->findFiles(QLatin1String("Morrowind"), path)) { QMessageBox msgBox; msgBox.setWindowTitle(tr("Destination not empty")); diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 0913cf9e4d..d9b2c3ba8f 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -127,6 +127,7 @@ void Wizard::MainWizard::setupPages() setPage(Page_Conclusion, new ConclusionPage(this)); setStartId(Page_Intro); } + void Wizard::MainWizard::accept() { writeSettings(); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 02e50a7e33..47d3c1be7c 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -43,7 +43,6 @@ Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : Wizard::UnshieldWorker::~UnshieldWorker() { - } void Wizard::UnshieldWorker::setInstallComponent(Wizard::Component component, bool install) @@ -198,6 +197,29 @@ void Wizard::UnshieldWorker::setupSettings() mIniSettings.readFile(stream); } +void Wizard::UnshieldWorker::writeSettings() +{ + if (getIniPath().isEmpty()) + return; + + QFile file(getIniPath()); + + if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) { + qDebug() << "Error opening .ini file!"; + emit error(tr("Failed to open Morrowind configuration file!"), + tr("Opening %1 failed: %2.").arg(getIniPath(), file.errorString())); + return; + } + + QTextStream stream(&file); + stream.setCodec(mIniCodec); + + if (!mIniSettings.writeFile(getIniPath(), stream)) { + emit error(tr("Failed to write Morrowind configuration file!"), + tr("Writing to %1 failed: %2.").arg(getIniPath(), file.errorString())); + } +} + bool Wizard::UnshieldWorker::removeDirectory(const QString &dirName) { bool result = true; @@ -383,6 +405,32 @@ void Wizard::UnshieldWorker::extract() setupAddon(Wizard::Component_Bloodmoon); } + // Update Morrowind configuration + if (getInstallComponent(Wizard::Component_Tribunal)) + { + mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); + } + + if (getInstallComponent(Wizard::Component_Bloodmoon)) + { + mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Bloodmoon.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Bloodmoon.esm"))); + } + + if (getInstallComponent(Wizard::Component_Tribunal) && + getInstallComponent(Wizard::Component_Bloodmoon)) + { + mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); + mIniSettings.setValue(QLatin1String("Archives/Archive1"), QVariant(QString("Bloodmoon.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); + mIniSettings.setValue(QLatin1String("Game Files/Game File2"), QVariant(QString("Bloodmoon.esm"))); + } + + + // Write the settings to the Morrowind config file + writeSettings(); + // Remove the temporary directory removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); @@ -554,6 +602,10 @@ bool Wizard::UnshieldWorker::installComponent(Component component) emit error(tr("Could not find Morrowind configuration file!"), tr("Failed to find %0.").arg(iniPath)); return false; } + + // Setup Morrowind configuration + setIniPath(getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); + setupSettings(); } if (component == Wizard::Component_Tribunal) @@ -564,7 +616,6 @@ bool Wizard::UnshieldWorker::installComponent(Component component) emit textChanged(tr("Extracting: Sound directory")); copyDirectory(sounds.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Sound")); } - } if (component == Wizard::Component_Bloodmoon) @@ -577,6 +628,15 @@ bool Wizard::UnshieldWorker::installComponent(Component component) copyFile(patch.absoluteFilePath(), original.absoluteFilePath()); } + // Load Morrowind configuration settings from the setup script + QFileInfo inx(disk.absoluteFilePath(QLatin1String("setup.inx"))); + + if (inx.exists()) { + emit textChanged(tr("Updating Morrowind configuration file")); + mIniSettings.parseInx(inx.absoluteFilePath()); + } else { + qDebug() << "setup.inx not found!"; + } } emit textChanged(tr("%0 installation finished!").arg(name)); @@ -649,7 +709,7 @@ bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fil { QString current(QString::fromLatin1(unshield_file_name(unshield, j))); - qDebug() << "File is: " << unshield_file_name(unshield, j); + qDebug() << "File is: " << current; if (current == fileName) return true; // File is found! } diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index d110851f4f..8aa54c9c06 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -41,8 +41,12 @@ namespace Wizard void setIniCodec(QTextCodec *codec); + void setupSettings(); + private: + void writeSettings(); + bool getInstallComponent(Component component); QString getComponentPath(Component component); @@ -58,8 +62,6 @@ namespace Wizard bool moveFile(const QString &source, const QString &destination); bool moveDirectory(const QString &source, const QString &destination); - void setupSettings(); - bool extractCab(const QString &cabFile, const QString &outputDir); bool extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter); bool findFile(const QString &cabFile, const QString &fileName); From 755c99df3ccd14db12a550f5e38e429f48909935 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 18 Feb 2014 12:44:27 +0100 Subject: [PATCH 031/303] Some minor fixes --- apps/wizard/componentselectionpage.cpp | 14 +++++------ apps/wizard/conclusionpage.cpp | 8 ++++++- apps/wizard/existinginstallationpage.cpp | 4 ++-- apps/wizard/inisettings.cpp | 30 ++++++++---------------- apps/wizard/installationpage.cpp | 14 +++++------ apps/wizard/installationtargetpage.cpp | 2 +- apps/wizard/methodselectionpage.cpp | 4 ++-- 7 files changed, 36 insertions(+), 40 deletions(-) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index 87779e93dd..19a07438db 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -16,7 +16,7 @@ Wizard::ComponentSelectionPage::ComponentSelectionPage(MainWizard *wizard) : setCommitPage(true); setButtonText(QWizard::CommitButton, tr("&Install")); - registerField("installation.components", componentsList); + registerField(QLatin1String("installation.components"), componentsList); connect(componentsList, SIGNAL(itemChanged(QListWidgetItem *)), this, SLOT(updateButton(QListWidgetItem *))); @@ -25,7 +25,7 @@ Wizard::ComponentSelectionPage::ComponentSelectionPage(MainWizard *wizard) : void Wizard::ComponentSelectionPage::updateButton(QListWidgetItem *item) { - if (field("installation.new").toBool() == true) + if (field(QLatin1String("installation.new")).toBool() == true) return; // Morrowind is always checked here bool unchecked = true; @@ -53,13 +53,13 @@ void Wizard::ComponentSelectionPage::initializePage() { componentsList->clear(); - QString path(field("installation.path").toString()); + QString path(field(QLatin1String("installation.path")).toString()); QListWidgetItem *morrowindItem = new QListWidgetItem(QLatin1String("Morrowind")); QListWidgetItem *tribunalItem = new QListWidgetItem(QLatin1String("Tribunal")); QListWidgetItem *bloodmoonItem = new QListWidgetItem(QLatin1String("Bloodmoon")); - if (field("installation.new").toBool() == true) + if (field(QLatin1String("installation.new")).toBool() == true) { morrowindItem->setFlags(morrowindItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); morrowindItem->setData(Qt::CheckStateRole, Qt::Checked); @@ -111,12 +111,12 @@ void Wizard::ComponentSelectionPage::initializePage() bool Wizard::ComponentSelectionPage::validatePage() { - QStringList components(field("installation.components").toStringList()); - QString path(field("installation.path").toString()); + QStringList components(field(QLatin1String("installation.components")).toStringList()); + QString path(field(QLatin1String("installation.path")).toString()); qDebug() << components << path << mWizard->mInstallations[path]; - if (field("installation.new").toBool() == false) { + if (field(QLatin1String("installation.new")).toBool() == false) { if (components.contains(QLatin1String("Tribunal")) && !components.contains(QLatin1String("Bloodmoon"))) { if (mWizard->mInstallations[path]->hasBloodmoon) diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index 61d58eaba1..d734d39ab8 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -13,9 +13,15 @@ Wizard::ConclusionPage::ConclusionPage(MainWizard *wizard) : void Wizard::ConclusionPage::initializePage() { + // Write the path to openmw.cfg + if (field(QLatin1String("installation.new")).toBool() == true) { + QString path(field(QLatin1String("installation.path")).toString()); + mWizard->addInstallation(path); + } + if (!mWizard->mError) { - if (field("installation.new").toBool() == true) + if (field(QLatin1String("installation.new")).toBool() == true) { textLabel->setText(tr("

The OpenMW Wizard successfully installed Morrowind on your computer.

\

Click Finish to close the Wizard.

")); diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 34e771e3d7..f0f5c8573e 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -82,7 +82,7 @@ bool Wizard::ExistingInstallationPage::validatePage() // It can be missing entirely // Or failed to be detected due to the target being a symlink - QString path(field("installation.path").toString()); + QString path(field(QLatin1String("installation.path")).toString()); QFile file(mWizard->mInstallations[path]->iniPath); if (!file.exists()) { @@ -131,7 +131,7 @@ bool Wizard::ExistingInstallationPage::isComplete() const int Wizard::ExistingInstallationPage::nextId() const { - QString path(field("installation.path").toString()); + QString path(field(QLatin1String("installation.path")).toString()); if (path.isEmpty()) return MainWizard::Page_LanguageSelection; diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp index f34699742b..a2c2556844 100644 --- a/apps/wizard/inisettings.cpp +++ b/apps/wizard/inisettings.cpp @@ -131,30 +131,20 @@ bool Wizard::IniSettings::writeFile(const QString &path, QTextStream &stream) int index = buffer.lastIndexOf(section); if (index != -1) { - // Append the new keys to the bottom of the section + // Look for the next section index = buffer.indexOf(QLatin1Char('['), index + 1); - if (index == -1 ) - { - // Beginning of next section not found, we are at the last section - if (buffer.lastIndexOf(QLatin1String("\n")) > (buffer.lastIndexOf(section) + section.length())) { - // There is a newline after the section - index = buffer.lastIndexOf(QLatin1String("\n")) - 1; - buffer.insert(index - 2, QString("\n%1=%2").arg(key, i.value().toString())); - mSettings.remove(i.key()); - continue; - } else { - // No newline found, or the last newline is before the last section - // Append the key to the bottom of the file - buffer.append(QString("\n%1=%2").arg(key, i.value().toString())); - mSettings.remove(i.key()); - continue; - } + if (index == -1 ) { + // We are at the last section, append it to the bottom of the file + buffer.append(QString("\n%1=%2").arg(key, i.value().toString())); + mSettings.remove(i.key()); + continue; + } else { + // Not at last section, add the key at the index + buffer.insert(index - 1, QString("\n%1=%2").arg(key, i.value().toString())); + mSettings.remove(i.key()); } - // Add the key at the index - buffer.insert(index - 1, QString("\n%1=%2").arg(key, i.value().toString())); - mSettings.remove(i.key()); } else { // Add the section to the end of the file, because it's not found buffer.append(QString("\n%1\n").arg(section)); diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 05d1b4af84..774e02f3c5 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -20,8 +20,8 @@ Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : void Wizard::InstallationPage::initializePage() { - QString path(field("installation.path").toString()); - QStringList components(field("installation.components").toStringList()); + QString path(field(QLatin1String("installation.path")).toString()); + QStringList components(field(QLatin1String("installation.components")).toStringList()); logTextEdit->appendPlainText(QString("Installing to %1").arg(path)); logTextEdit->appendPlainText(QString("Installing %1.").arg(components.join(", "))); @@ -32,7 +32,7 @@ void Wizard::InstallationPage::initializePage() // That way installing all three components would yield 300% // When one component is done the bar will be filled by 33% - if (field("installation.new").toBool() == true) { + if (field(QLatin1String("installation.new")).toBool() == true) { installProgressBar->setMaximum((components.count() * 100)); } else { if (components.contains(QLatin1String("Tribunal")) @@ -49,8 +49,8 @@ void Wizard::InstallationPage::initializePage() void Wizard::InstallationPage::startInstallation() { - QStringList components(field("installation.components").toStringList()); - QString path(field("installation.path").toString()); + QStringList components(field(QLatin1String("installation.components")).toStringList()); + QString path(field(QLatin1String("installation.path")).toString()); QThread *thread = new QThread(); mUnshield = new UnshieldWorker(); @@ -86,7 +86,7 @@ void Wizard::InstallationPage::startInstallation() connect(mUnshield, SIGNAL(requestFileDialog(Wizard::Component)), this, SLOT(showFileDialog(Wizard::Component)), Qt::QueuedConnection); - if (field("installation.new").toBool() == true) + if (field(QLatin1String("installation.new")).toBool() == true) { // Always install Morrowind mUnshield->setInstallComponent(Wizard::Component_Morrowind, true); @@ -117,7 +117,7 @@ void Wizard::InstallationPage::startInstallation() mUnshield->setPath(path); // Set the right codec to use for Morrowind.ini - QString language(field("installation.language").toString()); + QString language(field(QLatin1String("installation.language")).toString()); if (language == QLatin1String("Polish")) { mUnshield->setIniCodec(QTextCodec::codecForName("windows-1250")); diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index daf2c08467..521dc2ceaa 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -27,7 +27,7 @@ void Wizard::InstallationTargetPage::initializePage() bool Wizard::InstallationTargetPage::validatePage() { - QString path(field("installation.path").toString()); + QString path(field(QLatin1String("installation.path")).toString()); qDebug() << "Validating path: " << path; diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp index 6db0779a6a..b1483ac3a5 100644 --- a/apps/wizard/methodselectionpage.cpp +++ b/apps/wizard/methodselectionpage.cpp @@ -8,12 +8,12 @@ Wizard::MethodSelectionPage::MethodSelectionPage(MainWizard *wizard) : { setupUi(this); - registerField("installation.new", newLocationRadioButton); + registerField(QLatin1String("installation.new"), newLocationRadioButton); } int Wizard::MethodSelectionPage::nextId() const { - if (field("installation.new").toBool() == true) { + if (field(QLatin1String("installation.new")).toBool() == true) { return MainWizard::Page_InstallationTarget; } else { return MainWizard::Page_ExistingInstallation; From 36d4287da423cac163f3cc817639deaff6a1a281 Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 24 Feb 2014 15:59:44 +0100 Subject: [PATCH 032/303] Fixed openmw.cfg handling: file should not be cleared anymore --- apps/wizard/mainwizard.cpp | 64 ++++++++++++++++++++----- apps/wizard/unshield/unshieldworker.cpp | 21 ++++---- 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index d9b2c3ba8f..eb1e3b3045 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -41,9 +41,36 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : void Wizard::MainWizard::setupInstallations() { - QString userPath(QFile::decodeName(mCfgMgr.getUserConfigPath().string().c_str())); - QString globalPath(QFile::decodeName(mCfgMgr.getGlobalPath().string().c_str())); + QString userPath(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); + QString globalPath(QString::fromUtf8(mCfgMgr.getGlobalPath().string().c_str())); + QString message(tr("

Could not open %1 for reading

\ +

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

")); + // 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:" << qPrintable(path); + + if (file.exists()) { + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(message.arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + QTextStream stream(&file); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + + mGameSettings.readUserFile(stream); + } + + // Now the rest QStringList paths; paths.append(userPath + QLatin1String("openmw.cfg")); paths.append(QLatin1String("openmw.cfg")); @@ -59,10 +86,8 @@ void Wizard::MainWizard::setupInstallations() msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); - msgBox.exec(); + msgBox.setText(message.arg(file.fileName())); + return qApp->quit(); } QTextStream stream(&file); @@ -73,7 +98,7 @@ void Wizard::MainWizard::setupInstallations() file.close(); } - // Check if the paths actually contains a Morrowind installation + // Check if the paths actually contain a Morrowind installation foreach (const QString path, mGameSettings.getDataDirs()) { if (findFiles(QLatin1String("Morrowind"), path)) @@ -136,7 +161,24 @@ void Wizard::MainWizard::accept() void Wizard::MainWizard::writeSettings() { - QString userPath(QFile::decodeName(mCfgMgr.getUserConfigPath().string().c_str())); + QString userPath(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); + QDir dir(userPath); + + if (!dir.exists()) { + if (!dir.mkpath(userPath)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error creating OpenMW configuration directory")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not create %1

\ +

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

").arg(userPath)); + msgBox.exec(); + return qApp->quit(); + } + } + + // Game settings QFile file(userPath + QLatin1String("openmw.cfg")); if (!file.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) { @@ -145,9 +187,9 @@ void Wizard::MainWizard::writeSettings() msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Could not open or create %0 for writing

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); + msgBox.setText(tr("

Could not open %1 for writing

\ +

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

").arg(file.fileName())); msgBox.exec(); return qApp->quit(); } diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 47d3c1be7c..6a19673f3b 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -656,7 +656,7 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp path.append(prefix + QDir::separator()); if (directory >= 0) - path.append(QString::fromLatin1(unshield_directory_name(unshield, directory)) + QDir::separator()); + path.append(QString::fromUtf8(unshield_directory_name(unshield, directory)) + QDir::separator()); // Ensure the path has the right separators path.replace(QLatin1Char('\\'), QDir::separator()); @@ -667,7 +667,7 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp dir.mkpath(path); QString fileName(path); - fileName.append(QString::fromLatin1(unshield_file_name(unshield, index))); + fileName.append(QString::fromUtf8(unshield_file_name(unshield, index))); // Calculate the percentage done int progress = (((float) counter / (float) unshield_file_count(unshield)) * 100); @@ -678,13 +678,14 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp if (getComponentDone(Wizard::Component_Tribunal)) progress = progress + 100; - emit textChanged(tr("Extracting: %1").arg(QString::fromLatin1(unshield_file_name(unshield, index)))); + emit textChanged(tr("Extracting: %1").arg(QString::fromUtf8(unshield_file_name(unshield, index)))); emit progressChanged(progress); - success = unshield_file_save(unshield, index, fileName.toLatin1().constData()); + QByteArray array(fileName.toUtf8()); + success = unshield_file_save(unshield, index, array.constData()); if (!success) { - emit error(tr("Failed to extract %1.").arg(QString::fromLatin1(unshield_file_name(unshield, index))), tr("Complete path: %1.").arg(fileName)); + emit error(tr("Failed to extract %1.").arg(QString::fromUtf8(unshield_file_name(unshield, index))), tr("Complete path: %1.").arg(fileName)); dir.remove(fileName); } @@ -693,8 +694,10 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fileName) { + QByteArray array(cabFile.toUtf8()); + Unshield *unshield; - unshield = unshield_open(cabFile.toLatin1().constData()); + unshield = unshield_open(array.constData()); if (!unshield) { emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); @@ -707,7 +710,7 @@ bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fil for (size_t j=group->first_file; j<=group->last_file; ++j) { - QString current(QString::fromLatin1(unshield_file_name(unshield, j))); + QString current(QString::fromUtf8(unshield_file_name(unshield, j))); qDebug() << "File is: " << current; if (current == fileName) @@ -723,8 +726,10 @@ bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &o { bool success; + QByteArray array(cabFile.toUtf8()); + Unshield *unshield; - unshield = unshield_open(cabFile.toLatin1().constData()); + unshield = unshield_open(array.constData()); if (!unshield) { emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); From 3792b301e9378127d5fae3ddf9b2f4eefa602e73 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 25 Feb 2014 15:33:30 +0100 Subject: [PATCH 033/303] Wizard now runs the ini-importer to import settings from Morrowind.ini --- apps/launcher/maindialog.cpp | 106 ++------------------ apps/wizard/conclusionpage.cpp | 18 +++- apps/wizard/importpage.cpp | 3 + apps/wizard/inisettings.cpp | 126 ++++++++++++------------ apps/wizard/installationpage.cpp | 21 +++- apps/wizard/installationtargetpage.cpp | 29 +++++- apps/wizard/mainwizard.cpp | 83 ++++++++++++++-- apps/wizard/mainwizard.hpp | 2 + apps/wizard/unshield/unshieldworker.cpp | 27 ++--- components/CMakeLists.txt | 4 + components/config/gamesettings.hpp | 2 + components/process/processinvoker.cpp | 109 ++++++++++++++++++++ components/process/processinvoker.hpp | 23 +++++ files/ui/wizard/importpage.ui | 6 +- 14 files changed, 359 insertions(+), 200 deletions(-) create mode 100644 components/process/processinvoker.cpp create mode 100644 components/process/processinvoker.hpp diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 56b3186ff3..3b7a3273a5 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -1,6 +1,7 @@ #include "maindialog.hpp" #include +#include #include #include @@ -29,6 +30,8 @@ #include "graphicspage.hpp" #include "datafilespage.hpp" +using namespace Process; + Launcher::MainDialog::MainDialog(QWidget *parent) : mGameSettings(mCfgMgr), QMainWindow (parent) { @@ -273,7 +276,7 @@ bool Launcher::MainDialog::showFirstRunDialog() arguments.append(QString("--cfg")); arguments.append(path); - if (!startProgram(QString("mwiniimport"), arguments, false)) + if (!ProcessInvoker::startProcess(QLatin1String("mwiniimport"), arguments, false)) return false; // Re-read the game settings @@ -810,103 +813,6 @@ void Launcher::MainDialog::play() } // Launch the game detached - startProgram(QString("openmw"), true); - qApp->quit(); -} - -bool Launcher::MainDialog::startProgram(const QString &name, const QStringList &arguments, bool detached) -{ - QString path = name; -#ifdef Q_OS_WIN - path.append(QString(".exe")); -#elif defined(Q_OS_MAC) - QDir dir(QCoreApplication::applicationDirPath()); - path = dir.absoluteFilePath(name); -#else - path.prepend(QString("./")); -#endif - - QFile file(path); - - QProcess process; - QFileInfo info(file); - - if (!file.exists()) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error starting executable")); - msgBox.setIcon(QMessageBox::Warning); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Could not find %1

\ - The application is not found.
\ - Please make sure OpenMW is installed correctly and try again.
").arg(info.fileName())); - msgBox.exec(); - - return false; - } - - if (!info.isExecutable()) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error starting executable")); - msgBox.setIcon(QMessageBox::Warning); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Could not start %1

\ - The application is not executable.
\ - Please make sure you have the right permissions and try again.
").arg(info.fileName())); - msgBox.exec(); - - return false; - } - - // Start the executable - if (detached) { - if (!process.startDetached(path, arguments)) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error starting executable")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Could not start %1

\ - An error occurred while starting %1.

\ - Press \"Show Details...\" for more information.
").arg(info.fileName())); - msgBox.setDetailedText(process.errorString()); - msgBox.exec(); - - return false; - } - } else { - process.start(path, arguments); - if (!process.waitForFinished()) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error starting executable")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Could not start %1

\ - An error occurred while starting %1.

\ - Press \"Show Details...\" for more information.
").arg(info.fileName())); - msgBox.setDetailedText(process.errorString()); - msgBox.exec(); - - return false; - } - - if (process.exitCode() != 0 || process.exitStatus() == QProcess::CrashExit) { - QString error(process.readAllStandardError()); - error.append(tr("\nArguments:\n")); - error.append(arguments.join(" ")); - - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error running executable")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Executable %1 returned an error

\ - An error occurred while running %1.

\ - Press \"Show Details...\" for more information.
").arg(info.fileName())); - msgBox.setDetailedText(error); - msgBox.exec(); - - return false; - } - } - - return true; - + if (ProcessInvoker::startProcess(QLatin1String("openmw"), true)) + qApp->quit(); } diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index d734d39ab8..3681459b72 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -13,10 +13,20 @@ Wizard::ConclusionPage::ConclusionPage(MainWizard *wizard) : void Wizard::ConclusionPage::initializePage() { - // Write the path to openmw.cfg - if (field(QLatin1String("installation.new")).toBool() == true) { - QString path(field(QLatin1String("installation.path")).toString()); - mWizard->addInstallation(path); +// // Write the path to openmw.cfg +// if (field(QLatin1String("installation.new")).toBool() == true) { +// QString path(field(QLatin1String("installation.path")).toString()); +// mWizard->addInstallation(path); +// } + + if (!mWizard->mError) + { + if ((field(QLatin1String("installation.new")).toBool() == true) + | (field(QLatin1String("installation.import-settings")).toBool() == true)) + { + qDebug() << "IMPORT SETTINGS"; + mWizard->runSettingsImporter(); + } } if (!mWizard->mError) diff --git a/apps/wizard/importpage.cpp b/apps/wizard/importpage.cpp index 059683cf0a..b49105faa9 100644 --- a/apps/wizard/importpage.cpp +++ b/apps/wizard/importpage.cpp @@ -7,6 +7,9 @@ Wizard::ImportPage::ImportPage(MainWizard *wizard) : mWizard(wizard) { setupUi(this); + + registerField(QLatin1String("installation.import-settings"), importCheckBox); + registerField(QLatin1String("installation.import-addons"), addonsCheckBox); } int Wizard::ImportPage::nextId() const diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp index a2c2556844..8c733ce3b3 100644 --- a/apps/wizard/inisettings.cpp +++ b/apps/wizard/inisettings.cpp @@ -51,7 +51,7 @@ bool Wizard::IniSettings::readFile(QTextStream &stream) const QString line(stream.readLine()); if (line.isEmpty() || line.startsWith(QLatin1Char(';'))) - continue; + continue; if (sectionRe.exactMatch(line)) { @@ -91,83 +91,81 @@ bool Wizard::IniSettings::writeFile(const QString &path, QTextStream &stream) QString currentSection; QString buffer; - qDebug() << "Keys! " << keys; + while (!stream.atEnd()) { - while (!stream.atEnd()) { + const QString line(stream.readLine()); - const QString line(stream.readLine()); + if (line.isEmpty() || line.startsWith(QLatin1Char(';'))) { + buffer.append(line + QLatin1String("\n")); + continue; + } - if (line.isEmpty() || line.startsWith(QLatin1Char(';'))) { - buffer.append(line + QLatin1String("\n")); - continue; - } + if (sectionRe.exactMatch(line)) { + buffer.append(line + QLatin1String("\n")); + currentSection = sectionRe.cap(1); + } else if (keyRe.indexIn(line) != -1) { + QString key(keyRe.cap(1).trimmed()); + QString lookupKey(key); - if (sectionRe.exactMatch(line)) { - buffer.append(line + QLatin1String("\n")); - currentSection = sectionRe.cap(1); - } else if (keyRe.indexIn(line) != -1) { - QString key(keyRe.cap(1).trimmed()); - QString lookupKey(key); + // Append the section, but only if there is one + if (!currentSection.isEmpty()) + lookupKey = currentSection + QLatin1Char('/') + key; - // Append the section, but only if there is one - if (!currentSection.isEmpty()) - lookupKey = currentSection + QLatin1Char('/') + key; + buffer.append(key + QLatin1Char('=') + mSettings[lookupKey].toString() + QLatin1String("\n")); + mSettings.remove(lookupKey); + } + } - buffer.append(key + QLatin1Char('=') + mSettings[lookupKey].toString() + QLatin1String("\n")); - mSettings.remove(lookupKey); - } - } + // Add the new settings to the buffer + QHashIterator i(mSettings); + while (i.hasNext()) { + i.next(); - // Add the new settings to the buffer - QHashIterator i(mSettings); - while (i.hasNext()) { - i.next(); + QStringList fullKey(i.key().split(QLatin1Char('/'))); + QString section(fullKey.at(0)); + section.prepend(QLatin1Char('[')); + section.append(QLatin1Char(']')); + QString key(fullKey.at(1)); - QStringList fullKey(i.key().split(QLatin1Char('/'))); - QString section(fullKey.at(0)); - section.prepend(QLatin1Char('[')); - section.append(QLatin1Char(']')); - QString key(fullKey.at(1)); + int index = buffer.lastIndexOf(section); + if (index != -1) { + // Look for the next section + index = buffer.indexOf(QLatin1Char('['), index + 1); - int index = buffer.lastIndexOf(section); - if (index != -1) { - // Look for the next section - index = buffer.indexOf(QLatin1Char('['), index + 1); + if (index == -1 ) { + // We are at the last section, append it to the bottom of the file + buffer.append(QString("\n%1=%2").arg(key, i.value().toString())); + mSettings.remove(i.key()); + continue; + } else { + // Not at last section, add the key at the index + buffer.insert(index - 1, QString("\n%1=%2").arg(key, i.value().toString())); + mSettings.remove(i.key()); + } - if (index == -1 ) { - // We are at the last section, append it to the bottom of the file - buffer.append(QString("\n%1=%2").arg(key, i.value().toString())); - mSettings.remove(i.key()); - continue; - } else { - // Not at last section, add the key at the index - buffer.insert(index - 1, QString("\n%1=%2").arg(key, i.value().toString())); - mSettings.remove(i.key()); - } + } else { + // Add the section to the end of the file, because it's not found + buffer.append(QString("\n%1\n").arg(section)); + i.previous(); + } + } - } else { - // Add the section to the end of the file, because it's not found - buffer.append(QString("\n%1\n").arg(section)); - i.previous(); - } - } + // Now we reopen the file, this time we write + QFile file(path); - // Now we reopen the file, this time we write - QFile file(path); + if (file.open(QIODevice::ReadWrite | QIODevice::Truncate | QIODevice::Text)) { + QTextStream in(&file); + in.setCodec(stream.codec()); - if (file.open(QIODevice::ReadWrite | QIODevice::Truncate | QIODevice::Text)) { - QTextStream in(&file); - in.setCodec(stream.codec()); + // Write the updated buffer to an empty file + in << buffer; + file.flush(); + file.close(); + } else { + return false; + } - // Write the updated buffer to an empty file - in << buffer; - file.flush(); - file.close(); - } else { - return false; - } - - return true; + return true; } bool Wizard::IniSettings::parseInx(const QString &path) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 774e02f3c5..f104c81950 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -154,6 +154,11 @@ void Wizard::InstallationPage::showFileDialog(Wizard::Component component) if (fileName.isEmpty()) { qDebug() << "Cancel was clicked!"; + + logTextEdit->appendHtml(tr("


\ + Error: The installation was aborted by the user

")); + mWizard->mError = true; + emit completeChanged(); return; } @@ -184,8 +189,10 @@ void Wizard::InstallationPage::installationError(const QString &text, const QStr installProgressLabel->setText(tr("Installation failed!")); - logTextEdit->appendHtml(tr("Error: %1").arg(text)); - logTextEdit->appendHtml(tr("%1").arg(details)); + logTextEdit->appendHtml(tr("


\ + Error: %1

").arg(text)); + logTextEdit->appendHtml(tr("

\ + %1

").arg(details)); QMessageBox msgBox; msgBox.setWindowTitle(tr("An error occurred")); @@ -214,9 +221,13 @@ bool Wizard::InstallationPage::isComplete() const int Wizard::InstallationPage::nextId() const { - if (!mWizard->mError) { - return MainWizard::Page_Import; - } else { + if (field(QLatin1String("installation.new")).toBool() == true) { return MainWizard::Page_Conclusion; + } else { + if (!mWizard->mError) { + return MainWizard::Page_Import; + } else { + return MainWizard::Page_Conclusion; + } } } diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index 521dc2ceaa..d89d54a00b 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -31,11 +31,34 @@ bool Wizard::InstallationTargetPage::validatePage() qDebug() << "Validating path: " << path; - // TODO: Check writeability if (!QFile::exists(path)) { QDir dir; - dir.mkpath(path); - return true; + + if (!dir.mkpath(path)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error creating destination")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not create the destination directory

\ +

Please make sure you have the right permissions \ + and try again, or specify a different location.

")); + msgBox.exec(); + return false; + } + } + + QFileInfo info(path); + + if (!info.isWritable()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Insufficient permissions")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not write to the destination directory

\ +

Please make sure you have the right permissions \ + and try again, or specify a different location.

")); + msgBox.exec(); + return false; } if (mWizard->findFiles(QLatin1String("Morrowind"), path)) { diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index eb1e3b3045..fced2d39e6 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -1,5 +1,7 @@ #include "mainwizard.hpp" +#include + #include #include @@ -17,6 +19,8 @@ #include "importpage.hpp" #include "conclusionpage.hpp" +using namespace Process; + Wizard::MainWizard::MainWizard(QWidget *parent) : mGameSettings(mCfgMgr), QWizard(parent) @@ -35,11 +39,12 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setDefaultProperty("ComponentListWidget", "mCheckedItems", "checkedItemsChanged"); + setupGameSettings(); setupInstallations(); setupPages(); } -void Wizard::MainWizard::setupInstallations() +void Wizard::MainWizard::setupGameSettings() { QString userPath(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); QString globalPath(QString::fromUtf8(mCfgMgr.getGlobalPath().string().c_str())); @@ -97,14 +102,74 @@ void Wizard::MainWizard::setupInstallations() } file.close(); } +} +void Wizard::MainWizard::setupInstallations() +{ // Check if the paths actually contain a Morrowind installation foreach (const QString path, mGameSettings.getDataDirs()) { if (findFiles(QLatin1String("Morrowind"), path)) addInstallation(path); } +} +void Wizard::MainWizard::runSettingsImporter() +{ + QString path(field(QLatin1String("installation.path")).toString()); + + // Create the file if it doesn't already exist, else the importer will fail + QString userPath(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); + QFile file(userPath + QLatin1String("openmw.cfg")); + + 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 qApp->quit(); + } + + file.close(); + } + + // Construct the arguments to run the importer + QStringList arguments; + + // Import plugin selection? + if (field(QLatin1String("installation.import-addons")).toBool() == true) + arguments.append(QLatin1String("--game-files")); + + arguments.append(QLatin1String("--encoding")); + + // Set encoding + QString language(field(QLatin1String("installation.language")).toString()); + + if (language == QLatin1String("Polish")) { + arguments.append(QLatin1String("windows-1250")); + } else if (language == QLatin1String("Russian")) { + arguments.append(QLatin1String("windows-1251")); + } else { + arguments.append(QLatin1String("windows-1252")); + } + + // Now the paths + arguments.append(QLatin1String("--ini")); + arguments.append(path + QDir::separator() + QLatin1String("Morrowind.ini")); + arguments.append(QLatin1String("--cfg")); + arguments.append(userPath + QLatin1String("openmw.cfg")); + + if (!ProcessInvoker::startProcess(QLatin1String("mwiniimport"), arguments, false)) + return qApp->quit();; + + // Re-read the game settings + setupGameSettings(); } void Wizard::MainWizard::addInstallation(const QString &path) @@ -134,7 +199,7 @@ void Wizard::MainWizard::addInstallation(const QString &path) // Add it to the openmw.cfg too if (!mGameSettings.getDataDirs().contains(path)) { - mGameSettings.setMultiValue(QString("data"), path); + mGameSettings.setMultiValue(QLatin1String("data"), path); mGameSettings.addDataDir(path); } } @@ -161,6 +226,12 @@ void Wizard::MainWizard::accept() void Wizard::MainWizard::writeSettings() { + QString path(field(QLatin1String("installation.path")).toString()); + + // Make sure the installation path is the last data= entry + mGameSettings.removeDataDir(path); + mGameSettings.addDataDir(path); + QString userPath(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); QDir dir(userPath); @@ -209,10 +280,6 @@ bool Wizard::MainWizard::findFiles(const QString &name, const QString &path) return false; // TODO: add MIME handling to make sure the files are real - if (dir.exists(name + QLatin1String(".esm")) && dir.exists(name + QLatin1String(".bsa"))) { - return true; - } else { - return false; - } - + return (dir.entryList().contains(name + QLatin1String(".esm"), Qt::CaseInsensitive) + && dir.entryList().contains(name + QLatin1String(".bsa"), Qt::CaseInsensitive)); } diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index 31a93ffbbf..7520ba6374 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -39,6 +39,7 @@ namespace Wizard bool findFiles(const QString &name, const QString &path); void addInstallation(const QString &path); + void runSettingsImporter(); QMap mInstallations; @@ -48,6 +49,7 @@ namespace Wizard private: + void setupGameSettings(); void setupInstallations(); void setupPages(); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 6a19673f3b..64d15e92ae 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -477,7 +477,6 @@ void Wizard::UnshieldWorker::setupAddon(Component component) mWait.wait(&mLock); } else if (disk.exists(QLatin1String("data1.hdr"))) { - qDebug() << "Exists! " << disk.absolutePath(); setComponentPath(component, disk.absolutePath()); } @@ -532,13 +531,13 @@ bool Wizard::UnshieldWorker::installComponent(Component component) return false; } - emit textChanged(tr("Installing %0").arg(name)); + emit textChanged(tr("Installing %1").arg(name)); QDir disk(getComponentPath(component)); if (!disk.exists()) { qDebug() << "Component path not set: " << getComponentPath(Wizard::Component_Morrowind); - emit error(tr("Component path not set!"), tr("The source path for %0 was not set.").arg(name)); + emit error(tr("Component path not set!"), tr("The source path for %1 was not set.").arg(name)); return false; } @@ -552,20 +551,20 @@ bool Wizard::UnshieldWorker::installComponent(Component component) if (!temp.mkpath(tempPath)) { qDebug() << "Can't make path"; - emit error(tr("Cannot create temporary directory!"), tr("Failed to create %0.").arg(tempPath)); + emit error(tr("Cannot create temporary directory!"), tr("Failed to create %1.").arg(tempPath)); return false; } temp.setPath(tempPath); if (!temp.mkdir(name)) { - emit error(tr("Cannot create temporary directory!"), tr("Failed to create %0.").arg(temp.absoluteFilePath(name))); + emit error(tr("Cannot create temporary directory!"), tr("Failed to create %1.").arg(temp.absoluteFilePath(name))); return false; } if (!temp.cd(name)) { qDebug() << "Can't cd to dir"; - emit error(tr("Cannot move into temporary directory!"), tr("Failed to move into %0.").arg(temp.absoluteFilePath(name))); + emit error(tr("Cannot move into temporary directory!"), tr("Failed to move into %1.").arg(temp.absoluteFilePath(name))); return false; } @@ -578,7 +577,7 @@ bool Wizard::UnshieldWorker::installComponent(Component component) if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { qDebug() << "failed to move files!"; emit error(tr("Moving extracted files failed!"), - tr("Failed to move files from %0 to %1.").arg(temp.absoluteFilePath(QLatin1String("Data Files")), + tr("Failed to move files from %1 to %2.").arg(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())); return false; } @@ -599,7 +598,7 @@ bool Wizard::UnshieldWorker::installComponent(Component component) moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); } else { qDebug() << "Could not find ini file!"; - emit error(tr("Could not find Morrowind configuration file!"), tr("Failed to find %0.").arg(iniPath)); + emit error(tr("Could not find Morrowind configuration file!"), tr("Failed to find %1.").arg(iniPath)); return false; } @@ -639,7 +638,7 @@ bool Wizard::UnshieldWorker::installComponent(Component component) } } - emit textChanged(tr("%0 installation finished!").arg(name)); + emit textChanged(tr("%1 installation finished!").arg(name)); return true; } @@ -710,11 +709,13 @@ bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fil for (size_t j=group->first_file; j<=group->last_file; ++j) { - QString current(QString::fromUtf8(unshield_file_name(unshield, j))); - qDebug() << "File is: " << current; - if (current == fileName) - return true; // File is found! + if (unshield_file_is_valid(unshield, j)) { + QString current(QString::fromUtf8(unshield_file_name(unshield, j))); + + if (current.toLower() == fileName.toLower()) + return true; // File is found! + } } } diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 7115997879..9a15579631 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -105,6 +105,10 @@ if(QT_QTGUI_LIBRARY AND QT_QTCORE_LIBRARY) launchersettings settingsbase ) + + add_component_qt_dir (process + processinvoker + ) include(${QT_USE_FILE}) QT4_WRAP_UI(ESM_UI_HDR ${ESM_UI}) diff --git a/components/config/gamesettings.hpp b/components/config/gamesettings.hpp index 41335707da..c4a6ead79b 100644 --- a/components/config/gamesettings.hpp +++ b/components/config/gamesettings.hpp @@ -52,6 +52,8 @@ namespace Config } inline QStringList getDataDirs() { return mDataDirs; } + + inline void removeDataDir(const QString &dir) { if(!dir.isEmpty()) mDataDirs.removeAll(dir); } inline void addDataDir(const QString &dir) { if(!dir.isEmpty()) mDataDirs.append(dir); } inline QString getDataLocal() {return mDataLocal; } diff --git a/components/process/processinvoker.cpp b/components/process/processinvoker.cpp new file mode 100644 index 0000000000..e75daae9b3 --- /dev/null +++ b/components/process/processinvoker.cpp @@ -0,0 +1,109 @@ +#include "processinvoker.hpp" + +#include +#include +#include +#include +#include +#include +#include + +Process::ProcessInvoker::ProcessInvoker() +{ +} + +Process::ProcessInvoker::~ProcessInvoker() +{ +} + +bool Process::ProcessInvoker::startProcess(const QString &name, const QStringList &arguments, bool detached) +{ + QString path(name); +#ifdef Q_OS_WIN + path.append(QLatin1String(".exe")); +#elif defined(Q_OS_MAC) + QDir dir(QCoreApplication::applicationDirPath()); + path = dir.absoluteFilePath(name); +#else + path.prepend(QLatin1String("./")); +#endif + + QProcess process; + QFileInfo info(path); + + if (!info.exists()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error starting executable")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not find %1

\ +

The application is not found.

\ +

Please make sure OpenMW is installed correctly and try again.

").arg(info.fileName())); + msgBox.exec(); + return false; + } + + if (!info.isExecutable()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error starting executable")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not start %1

\ +

The application is not executable.

\ +

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

").arg(info.fileName())); + msgBox.exec(); + return false; + } + + // Start the executable + if (detached) { + if (!process.startDetached(path, arguments)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error starting executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not start %1

\ +

An error occurred while starting %1.

\ +

Press \"Show Details...\" for more information.

").arg(info.fileName())); + msgBox.setDetailedText(process.errorString()); + msgBox.exec(); + return false; + } + } else { + process.start(path, arguments); + if (!process.waitForFinished()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error starting executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not start %1

\ +

An error occurred while starting %1.

\ +

Press \"Show Details...\" for more information.

").arg(info.fileName())); + msgBox.setDetailedText(process.errorString()); + msgBox.exec(); + + return false; + } + + if (process.exitCode() != 0 || process.exitStatus() == QProcess::CrashExit) { + QString error(process.readAllStandardError()); + error.append(tr("\nArguments:\n")); + error.append(arguments.join(" ")); + + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error running executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Executable %1 returned an error

\ +

An error occurred while running %1.

\ +

Press \"Show Details...\" for more information.

").arg(info.fileName())); + msgBox.setDetailedText(error); + msgBox.exec(); + + return false; + } + } + + return true; + +} diff --git a/components/process/processinvoker.hpp b/components/process/processinvoker.hpp new file mode 100644 index 0000000000..d59d2f0121 --- /dev/null +++ b/components/process/processinvoker.hpp @@ -0,0 +1,23 @@ +#ifndef PROCESSINVOKER_HPP +#define PROCESSINVOKER_HPP + +#include +#include + +namespace Process +{ + class ProcessInvoker : public QObject + { + Q_OBJECT + + ProcessInvoker(); + ~ProcessInvoker(); + + public: + + inline static bool startProcess(const QString &name, bool detached = false) { return startProcess(name, QStringList(), detached); } + bool static startProcess(const QString &name, const QStringList &arguments, bool detached = false); + }; +} + +#endif // PROCESSINVOKER_HPP diff --git a/files/ui/wizard/importpage.ui b/files/ui/wizard/importpage.ui index 5b078efca7..52f9ac4f2e 100644 --- a/files/ui/wizard/importpage.ui +++ b/files/ui/wizard/importpage.ui @@ -6,8 +6,8 @@ 0 0 - 511 - 326 + 510 + 324 @@ -43,7 +43,7 @@ - Import add-on and plugin selection (creates a new Content List in the launcher) + Import add-on and plugin selection From d30c4edb14f789b9a5c3c69d95612a3bbe49d8ef Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 25 Feb 2014 17:23:26 +0100 Subject: [PATCH 034/303] Support separate Splash directory, found in some installation media cabinets --- apps/wizard/unshield/unshieldworker.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 64d15e92ae..ea09650f63 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -587,6 +587,14 @@ bool Wizard::UnshieldWorker::installComponent(Component component) if (component == Wizard::Component_Morrowind) { + // Some installations have a separate Splash directory + QFileInfo splash(temp.absoluteFilePath(QLatin1String("Splash"))); + + if (splash.exists()) { + emit textChanged(tr("Extracting: Splash directory")); + copyDirectory(splash.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Splash")); + } + // Copy Morrowind configuration file QString iniPath(temp.absoluteFilePath(QLatin1String("App Executables"))); iniPath.append(QDir::separator() + QLatin1String("Morrowind.ini")); From 6b9082c6ba1098f95c96c76875cb112982a7d15e Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 25 Feb 2014 17:36:55 +0100 Subject: [PATCH 035/303] Support different Tribunal patch .esm location, as seen on German GoTY disk --- apps/wizard/unshield/unshieldworker.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index ea09650f63..617049daf1 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -630,6 +630,10 @@ bool Wizard::UnshieldWorker::installComponent(Component component) QFileInfo patch(temp.absoluteFilePath(QLatin1String("Tribunal Patch") + QDir::separator() + QLatin1String("Tribunal.esm"))); QFileInfo original(getPath() + QDir::separator() + QLatin1String("Tribunal.esm")); + // Look for the patch in other places too, it's not always in "Tribunal Patch" + if (!patch.exists()) + patch = QFileInfo(temp.absoluteFilePath(QLatin1String("Tribunal") + QDir::separator() + QLatin1String("Tribunal.esm"))); + if (original.exists() && patch.exists()) { emit textChanged(tr("Extracting: Tribunal patch")); copyFile(patch.absoluteFilePath(), original.absoluteFilePath()); From 05f8af07576ae565969379f2074c5d732cf8b596 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 25 Feb 2014 18:15:29 +0100 Subject: [PATCH 036/303] Use the correct encoding for the ini importer --- apps/wizard/mainwizard.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index fced2d39e6..8b95ecfa10 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -152,11 +152,11 @@ void Wizard::MainWizard::runSettingsImporter() QString language(field(QLatin1String("installation.language")).toString()); if (language == QLatin1String("Polish")) { - arguments.append(QLatin1String("windows-1250")); + arguments.append(QLatin1String("win1250")); } else if (language == QLatin1String("Russian")) { - arguments.append(QLatin1String("windows-1251")); + arguments.append(QLatin1String("win1251")); } else { - arguments.append(QLatin1String("windows-1252")); + arguments.append(QLatin1String("win1252")); } // Now the paths From b2156351d8852fc32ac6f83404651befa333fb33 Mon Sep 17 00:00:00 2001 From: pvdk Date: Sun, 16 Mar 2014 20:07:54 +0100 Subject: [PATCH 037/303] Work in progress commit, working on data1.hdr autodetection --- apps/wizard/existinginstallationpage.cpp | 1 - apps/wizard/inisettings.cpp | 2 +- apps/wizard/installationpage.cpp | 2 + apps/wizard/installationpage.hpp | 1 - apps/wizard/mainwizard.cpp | 13 + apps/wizard/mainwizard.hpp | 1 + apps/wizard/unshield/unshieldworker.cpp | 449 +++++++++++++++-------- apps/wizard/unshield/unshieldworker.hpp | 12 +- 8 files changed, 315 insertions(+), 166 deletions(-) diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index f0f5c8573e..91d4f2df1c 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -61,7 +61,6 @@ void Wizard::ExistingInstallationPage::textChanged(const QString &text) void Wizard::ExistingInstallationPage::initializePage() { - QStringList paths(mWizard->mInstallations.keys()); if (paths.isEmpty()) diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp index 8c733ce3b3..f67da04557 100644 --- a/apps/wizard/inisettings.cpp +++ b/apps/wizard/inisettings.cpp @@ -66,7 +66,7 @@ bool Wizard::IniSettings::readFile(QTextStream &stream) if (!currentSection.isEmpty()) key = currentSection + QLatin1Char('/') + key; - qDebug() << "adding: " << key << value; + //qDebug() << "adding: " << key << value; mSettings[key] = QVariant(value); } } diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index f104c81950..33ceadf8c7 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -44,6 +44,8 @@ void Wizard::InstallationPage::initializePage() installProgressBar->setMaximum(installProgressBar->maximum() + 100); } + // Handle when cancel is clicked + startInstallation(); } diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index 7001b26308..5e58477bed 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -42,7 +42,6 @@ namespace Wizard protected: void initializePage(); - }; } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 8b95ecfa10..e6dfc67d1d 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -224,6 +224,19 @@ void Wizard::MainWizard::accept() QWizard::accept(); } +void Wizard::MainWizard::reject() +{ + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Quit Wizard")); + msgBox.setIcon(QMessageBox::Question); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setText(tr("Are you sure you want to exit the Wizard?")); + + if (msgBox.exec() == QMessageBox::Yes) { + QWizard::reject(); + } +} + void Wizard::MainWizard::writeSettings() { QString path(field(QLatin1String("installation.path")).toString()); diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index 7520ba6374..962571ac83 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -59,6 +59,7 @@ namespace Wizard private slots: void accept(); + void reject(); }; diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 617049daf1..c237389436 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -319,144 +319,171 @@ bool Wizard::UnshieldWorker::moveDirectory(const QString &source, const QString return copyDirectory(source, destination, false); } -void Wizard::UnshieldWorker::installDirectories(const QString &source) +bool Wizard::UnshieldWorker::installFile(const QString &fileName, const QString &path) { - QDir dir(source); + qDebug() << "Attempting to find file: " << fileName << " in: " << path; + + bool result = true; + QDir dir(path); if (!dir.exists()) - return; - - QStringList directories; - directories << QLatin1String("Fonts") - << QLatin1String("Music") - << QLatin1String("Sound") - << QLatin1String("Splash") - << QLatin1String("Video"); + return false; QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | - QDir::System | QDir::Hidden | - QDir::AllDirs)); + QDir::System | QDir::Hidden | + QDir::AllDirs | QDir::Files, QDir::DirsFirst)); + foreach(QFileInfo info, list) { + if (info.isDir()) { + result = installFile(fileName, info.absoluteFilePath()); + } else { + if (info.fileName() == fileName) { + qDebug() << "File found at: " << info.absoluteFilePath(); + + emit textChanged(tr("Installing: %1").arg(info.fileName())); + return moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName()); + } + } + } + + return result; +} + +bool Wizard::UnshieldWorker::installDirectory(const QString &dirName, const QString &path, bool recursive) +{ + qDebug() << "Attempting to find: " << dirName << " in: " << path; + bool result = true; + QDir dir(path); + + if (!dir.exists()) + return false; + + QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | + QDir::System | QDir::Hidden | + QDir::AllDirs)); foreach(QFileInfo info, list) { if (info.isSymLink()) continue; - if (directories.contains(info.fileName())) { - qDebug() << "found " << info.fileName(); - emit textChanged(tr("Extracting: %1 directory").arg(info.fileName())); - copyDirectory(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName()); + if (info.isDir()) { + if (info.fileName() == dirName) { + qDebug() << "Directory found at: " << info.absoluteFilePath(); + emit textChanged(tr("Installing: %1 directory").arg(info.fileName())); + return copyDirectory(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName()); + } else { + if (recursive) + result = installDirectory(dirName, info.absoluteFilePath()); + } } } - // Copy the Data Files dir too, but only the subdirectories - QFileInfo info(dir.absoluteFilePath("Data Files")); - if (info.exists()) { - emit textChanged(tr("Extracting: Data Files directory")); - copyDirectory(info.absoluteFilePath(), getPath()); - } - + return result; } void Wizard::UnshieldWorker::extract() { qDebug() << "extract!"; - QDir disk; - if (getInstallComponent(Wizard::Component_Morrowind)) - { - while (!getComponentDone(Wizard::Component_Morrowind)) - { - if (getComponentPath(Wizard::Component_Morrowind).isEmpty()) { - qDebug() << "request file dialog"; - QReadLocker readLock(&mLock); - emit requestFileDialog(Wizard::Component_Morrowind); - mWait.wait(&mLock); - } + qDebug() << findFiles(QLatin1String("data1.hdr"), QLatin1String("/mnt/cdrom")); +// QDir disk; - if (!getComponentPath(Wizard::Component_Morrowind).isEmpty()) { - disk.setPath(getComponentPath(Wizard::Component_Morrowind)); +// if (getInstallComponent(Wizard::Component_Morrowind)) +// { +// if (!getComponentDone(Wizard::Component_Morrowind)) +// { +// if (getComponentPath(Wizard::Component_Morrowind).isEmpty()) { +// qDebug() << "request file dialog"; +// QReadLocker readLock(&mLock); +// emit requestFileDialog(Wizard::Component_Morrowind); +// mWait.wait(&mLock); +// } - if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Morrowind.bsa")) - | findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Tribunal.bsa")) - | findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Bloodmoon.bsa"))) - { - QReadLocker readLock(&mLock); - emit requestFileDialog(Wizard::Component_Morrowind); - mWait.wait(&mLock); - } else { - if (installComponent(Wizard::Component_Morrowind)) { - setComponentDone(Wizard::Component_Morrowind, true); - } else { - qDebug() << "Erorr installing Morrowind"; +// if (!getComponentPath(Wizard::Component_Morrowind).isEmpty()) { +// disk.setPath(getComponentPath(Wizard::Component_Morrowind)); - return; - } - } - } - } - } +// if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Morrowind.bsa"))) +// { +// QReadLocker readLock(&mLock); +// emit requestFileDialog(Wizard::Component_Morrowind); +// mWait.wait(&mLock); +// } else { +// if (installComponent(Wizard::Component_Morrowind)) { +// setComponentDone(Wizard::Component_Morrowind, true); +// } else { +// qDebug() << "Erorr installing Morrowind"; - if (getInstallComponent(Wizard::Component_Tribunal)) - { - setupAddon(Wizard::Component_Tribunal); - } +// return; +// } +// } +// } +// } +// } - if (getInstallComponent(Wizard::Component_Bloodmoon)) - { - setupAddon(Wizard::Component_Bloodmoon); - } +// if (getInstallComponent(Wizard::Component_Tribunal)) +// { +// setupAddon(Wizard::Component_Tribunal); +// } - // Update Morrowind configuration - if (getInstallComponent(Wizard::Component_Tribunal)) - { - mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); - mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); - } +// if (getInstallComponent(Wizard::Component_Bloodmoon)) +// { +// setupAddon(Wizard::Component_Bloodmoon); +// } - if (getInstallComponent(Wizard::Component_Bloodmoon)) - { - mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Bloodmoon.bsa"))); - mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Bloodmoon.esm"))); - } +// // Update Morrowind configuration +// if (getInstallComponent(Wizard::Component_Tribunal)) +// { +// mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); +// mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); +// } - if (getInstallComponent(Wizard::Component_Tribunal) && - getInstallComponent(Wizard::Component_Bloodmoon)) - { - mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); - mIniSettings.setValue(QLatin1String("Archives/Archive1"), QVariant(QString("Bloodmoon.bsa"))); - mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); - mIniSettings.setValue(QLatin1String("Game Files/Game File2"), QVariant(QString("Bloodmoon.esm"))); - } +// if (getInstallComponent(Wizard::Component_Bloodmoon)) +// { +// mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Bloodmoon.bsa"))); +// mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Bloodmoon.esm"))); +// } + +// if (getInstallComponent(Wizard::Component_Tribunal) && +// getInstallComponent(Wizard::Component_Bloodmoon)) +// { +// mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); +// mIniSettings.setValue(QLatin1String("Archives/Archive1"), QVariant(QString("Bloodmoon.bsa"))); +// mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); +// mIniSettings.setValue(QLatin1String("Game Files/Game File2"), QVariant(QString("Bloodmoon.esm"))); +// } - // Write the settings to the Morrowind config file - writeSettings(); +// // Write the settings to the Morrowind config file +// writeSettings(); - // Remove the temporary directory - removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); +// // Remove the temporary directory +// //removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); - // Fill the progress bar - int total = 0; +// // Fill the progress bar +// int total = 0; - if (getInstallComponent(Wizard::Component_Morrowind)) - total = 100; +// if (getInstallComponent(Wizard::Component_Morrowind)) +// total = 100; - if (getInstallComponent(Wizard::Component_Tribunal)) - total = total + 100; +// if (getInstallComponent(Wizard::Component_Tribunal)) +// total = total + 100; - if (getInstallComponent(Wizard::Component_Bloodmoon)) - total = total + 100; +// if (getInstallComponent(Wizard::Component_Bloodmoon)) +// total = total + 100; - emit textChanged(tr("Installation finished!")); - emit progressChanged(total); - emit finished(); +// emit textChanged(tr("Installation finished!")); +// emit progressChanged(total); +// emit finished(); - qDebug() << "installation finished!"; +// qDebug() << "installation finished!"; } void Wizard::UnshieldWorker::setupAddon(Component component) { - while (!getComponentDone(component)) + qDebug() << "SetupAddon!" << getComponentPath(component) << getComponentPath(Wizard::Component_Morrowind); + + if (!getComponentDone(component)) { + qDebug() << "Component not done!"; + QDir disk(getComponentPath(Wizard::Component_Morrowind)); QString name; if (component == Wizard::Component_Tribunal) @@ -470,43 +497,72 @@ void Wizard::UnshieldWorker::setupAddon(Component component) return; } - if (!disk.cd(name)) { - qDebug() << "not found on cd!"; + qDebug() << "Determine if file is in current data1.hdr: " << name; + + if (!disk.isEmpty()) { + if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) + { + if (!disk.cd(name)) { + qDebug() << "not found on cd!"; + QReadLocker locker(&mLock); + emit requestFileDialog(component); + mWait.wait(&mLock); + + } else if (disk.exists(QLatin1String("data1.hdr"))) { + if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) + { + QReadLocker locker(&mLock); + emit requestFileDialog(component); + mWait.wait(&mLock); + } else { + setComponentPath(component, disk.absolutePath()); + disk.setPath(getComponentPath(component)); + } + } + } + + } else { QReadLocker locker(&mLock); emit requestFileDialog(component); mWait.wait(&mLock); - - } else if (disk.exists(QLatin1String("data1.hdr"))) { - setComponentPath(component, disk.absolutePath()); } - if (getComponentPath(component).isEmpty()) { - qDebug() << "request file dialog"; - QReadLocker locker(&mLock); - emit requestFileDialog(Wizard::Component_Tribunal); - mWait.wait(&mLock); - } - - // Make sure the dir is up-to-date disk.setPath(getComponentPath(component)); - if (!getComponentPath(component).isEmpty()) { - - if (!findFile(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) - { + if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) + { + if (!disk.cd(name)) { + qDebug() << "not found on cd!"; QReadLocker locker(&mLock); emit requestFileDialog(component); mWait.wait(&mLock); - } else { - // Now do the actual installing - if (installComponent(component)) { - setComponentDone(component, true); + + } else if (disk.exists(QLatin1String("data1.hdr"))) { + if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) + { + QReadLocker locker(&mLock); + emit requestFileDialog(component); + mWait.wait(&mLock); } else { - qDebug() << "Error installing " << name; - return; + setComponentPath(component, disk.absolutePath()); + disk.setPath(getComponentPath(component)); } } + + // Make sure the dir is up-to-date + //disk.setPath(getComponentPath(component)); } + + // Now do the actual installing + + if (installComponent(component)) { + setComponentDone(component, true); + } else { + qDebug() << "Error installing " << name; + return; + } + + } } @@ -573,40 +629,60 @@ bool Wizard::UnshieldWorker::installComponent(Component component) return false; // Move the files from the temporary path to the destination folder +// emit textChanged(tr("Moving installation files")); +// if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { +// qDebug() << "failed to move files!"; +// emit error(tr("Moving extracted files failed!"), +// tr("Failed to move files from %1 to %2.").arg(temp.absoluteFilePath(QLatin1String("Data Files")), +// getPath())); +// return false; +// } emit textChanged(tr("Moving installation files")); - if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { - qDebug() << "failed to move files!"; - emit error(tr("Moving extracted files failed!"), - tr("Failed to move files from %1 to %2.").arg(temp.absoluteFilePath(QLatin1String("Data Files")), - getPath())); - return false; + + // Install extracted directories + QStringList directories; + directories << QLatin1String("BookArt") + << QLatin1String("Fonts") + << QLatin1String("Icons") + << QLatin1String("Meshes") + << QLatin1String("Music") + << QLatin1String("Sound") + << QLatin1String("Splash") + << QLatin1String("Textures") + << QLatin1String("Video"); + + foreach (const QString &dir, directories) { + installDirectory(dir, temp.absolutePath()); } - // Install files outside of cab archives - installDirectories(disk.absolutePath()); + // Install directories from disk + foreach (const QString &dir, directories) { + qDebug() << "\n\nDISK DIRS!"; + installDirectory(dir, disk.absolutePath(), false); + } + + QFileInfo info(disk.absoluteFilePath("Data Files")); + if (info.exists()) { + emit textChanged(tr("Installing: Data Files directory")); + copyDirectory(info.absoluteFilePath(), getPath()); + } if (component == Wizard::Component_Morrowind) { - // Some installations have a separate Splash directory - QFileInfo splash(temp.absoluteFilePath(QLatin1String("Splash"))); + QStringList files; + files << QLatin1String("Morrowind.esm") + << QLatin1String("Morrowin.bsa"); - if (splash.exists()) { - emit textChanged(tr("Extracting: Splash directory")); - copyDirectory(splash.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Splash")); + foreach (const QString &file, files) { + if (!installFile(file, temp.absolutePath())) { + emit error(tr("Could not find Morrowind data file!"), tr("Failed to find %1.").arg(file)); + return false; + } } // Copy Morrowind configuration file - QString iniPath(temp.absoluteFilePath(QLatin1String("App Executables"))); - iniPath.append(QDir::separator() + QLatin1String("Morrowind.ini")); - - QFileInfo info(iniPath); - - if (info.exists()) { - emit textChanged(tr("Extracting: Morrowind.ini")); - moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); - } else { - qDebug() << "Could not find ini file!"; - emit error(tr("Could not find Morrowind configuration file!"), tr("Failed to find %1.").arg(iniPath)); + if (!installFile(QLatin1String("Morrowind.ini"), temp.absolutePath())) { + emit error(tr("Could not find Morrowind configuration file!"), tr("Failed to find %1.").arg(QLatin1String("Morrowind.ini"))); return false; } @@ -620,23 +696,43 @@ bool Wizard::UnshieldWorker::installComponent(Component component) QFileInfo sounds(temp.absoluteFilePath(QLatin1String("Sounds"))); if (sounds.exists()) { - emit textChanged(tr("Extracting: Sound directory")); + emit textChanged(tr("Installing: Sound directory")); copyDirectory(sounds.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Sound")); } + + QStringList files; + files << QLatin1String("Tribunal.esm") + << QLatin1String("Tribunal.bsa"); + + foreach (const QString &file, files) { + if (!installFile(file, temp.absolutePath())) { + emit error(tr("Could not find Tribunal data file!"), tr("Failed to find %1.").arg(file)); + return false; + } + } + } if (component == Wizard::Component_Bloodmoon) { - QFileInfo patch(temp.absoluteFilePath(QLatin1String("Tribunal Patch") + QDir::separator() + QLatin1String("Tribunal.esm"))); QFileInfo original(getPath() + QDir::separator() + QLatin1String("Tribunal.esm")); - // Look for the patch in other places too, it's not always in "Tribunal Patch" - if (!patch.exists()) - patch = QFileInfo(temp.absoluteFilePath(QLatin1String("Tribunal") + QDir::separator() + QLatin1String("Tribunal.esm"))); + if (original.exists()) { + if (!installFile(QLatin1String("Tribunal.esm"), temp.absolutePath())) { + emit error(tr("Could not find Tribunal patch file!"), tr("Failed to find %1.").arg(QLatin1String("Tribunal.esm"))); + return false; + } + } - if (original.exists() && patch.exists()) { - emit textChanged(tr("Extracting: Tribunal patch")); - copyFile(patch.absoluteFilePath(), original.absoluteFilePath()); + QStringList files; + files << QLatin1String("Bloodmoon.esm") + << QLatin1String("Bloodmoon.bsa"); + + foreach (const QString &file, files) { + if (!installFile(file, temp.absolutePath())) { + emit error(tr("Could not find Bloodmoon data file!"), tr("Failed to find %1.").arg(file)); + return false; + } } // Load Morrowind configuration settings from the setup script @@ -655,10 +751,10 @@ bool Wizard::UnshieldWorker::installComponent(Component component) } -bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter) +bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &destination, const QString &prefix, int index, int counter) { bool success; - QString path(outputDir); + QString path(destination); path.append(QDir::separator()); int directory = unshield_file_directory(unshield, index); @@ -703,7 +799,42 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &outp return success; } -bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fileName) +QString Wizard::UnshieldWorker::findFile(const QString &fileName, const QString &path) +{ + return findFiles(fileName, path).first(); +} + +QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QString &path) +{ + QStringList result; + QDir dir(source); + + if (!dir.exists()) + return QStringList(); + + QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | + QDir::System | QDir::Hidden | + QDir::AllDirs | QDir::Files, QDir::DirsFirst)); + foreach(QFileInfo info, list) { + + if (info.isSymLink()) + continue; + + if (info.isDir()) { + result = findFiles(file, info.absoluteFilePath()); + } else { + if (info.fileName() == fileName) { + qDebug() << "File found at: " << info.absoluteFilePath(); + result.append(info.absoluteFilePath()); + } + } + } + + return result; + +} + +bool Wizard::UnshieldWorker::findInCab(const QString &cabFile, const QString &fileName) { QByteArray array(cabFile.toUtf8()); @@ -724,7 +855,7 @@ bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fil if (unshield_file_is_valid(unshield, j)) { QString current(QString::fromUtf8(unshield_file_name(unshield, j))); - + qDebug() << "Current is: " << current; if (current.toLower() == fileName.toLower()) return true; // File is found! } @@ -735,7 +866,7 @@ bool Wizard::UnshieldWorker::findFile(const QString &cabFile, const QString &fil return false; } -bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &outputDir) +bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &destination) { bool success; @@ -758,7 +889,7 @@ bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &o for (size_t j=group->first_file; j<=group->last_file; ++j) { if (unshield_file_is_valid(unshield, j)) { - success = extractFile(unshield, outputDir, group->name, j, counter); + success = extractFile(unshield, destination, group->name, j, counter); ++counter; } } diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index 8aa54c9c06..74ae6c6d85 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -62,11 +62,15 @@ namespace Wizard bool moveFile(const QString &source, const QString &destination); bool moveDirectory(const QString &source, const QString &destination); - bool extractCab(const QString &cabFile, const QString &outputDir); - bool extractFile(Unshield *unshield, const QString &outputDir, const QString &prefix, int index, int counter); - bool findFile(const QString &cabFile, const QString &fileName); + bool extractCab(const QString &cabFile, const QString &destination); + bool extractFile(Unshield *unshield, const QString &destination, const QString &prefix, int index, int counter); + bool findInCab(const QString &cabFile, const QString &fileName); - void installDirectories(const QString &source); + QString findFile(const QString &fileName, const QString &path); + QStringList findFiles(const QString &fileName, const QString &path); + + bool installFile(const QString &fileName, const QString &path); + bool installDirectory(const QString &dirName, const QString &path, bool recursive = true); bool installMorrowind(); bool installTribunal(); From ef16b461483284cb5044cfc2348d2cd93f48339e Mon Sep 17 00:00:00 2001 From: pvdk Date: Sun, 16 Mar 2014 22:09:20 +0100 Subject: [PATCH 038/303] Wizard now autodetects correct installation archive --- apps/wizard/installationpage.cpp | 20 +- apps/wizard/unshield/unshieldworker.cpp | 439 ++++++++++-------------- apps/wizard/unshield/unshieldworker.hpp | 23 +- 3 files changed, 203 insertions(+), 279 deletions(-) diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 33ceadf8c7..3fcbd1fe29 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -148,13 +148,18 @@ void Wizard::InstallationPage::showFileDialog(Wizard::Component component) break; } - QString fileName = QFileDialog::getOpenFileName( - this, - tr("Select %1 installation file").arg(name), - QDir::rootPath(), - tr("InstallShield header files (*.hdr)")); +// QString fileName = QFileDialog::getOpenFileName( +// this, +// tr("Select %1 installation file").arg(name), +// QDir::rootPath(), +// tr("InstallShield header files (*.hdr)")); - if (fileName.isEmpty()) { + + QString path = QFileDialog::getExistingDirectory(this, + tr("Select %1 installation media").arg(name), + QDir::rootPath()); + + if (path.isEmpty()) { qDebug() << "Cancel was clicked!"; logTextEdit->appendHtml(tr("


\ @@ -164,8 +169,7 @@ void Wizard::InstallationPage::showFileDialog(Wizard::Component component) return; } - QFileInfo info(fileName); - mUnshield->setComponentPath(component, info.absolutePath()); + mUnshield->setDiskPath(path); } void Wizard::InstallationPage::installationFinished() diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index c237389436..223de41ce4 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -20,12 +20,9 @@ Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : { unshield_set_log_level(0); - mMorrowindPath = QString(); - mTribunalPath = QString(); - mBloodmoonPath = QString(); - mPath = QString(); mIniPath = QString(); + mDiskPath = QString(); // Default to Latin encoding mIniCodec = QTextCodec::codecForName("windows-1252"); @@ -78,41 +75,6 @@ bool Wizard::UnshieldWorker::getInstallComponent(Component component) return false; } -void Wizard::UnshieldWorker::setComponentPath(Wizard::Component component, const QString &path) -{ - QWriteLocker writeLock(&mLock); - switch (component) { - - case Wizard::Component_Morrowind: - mMorrowindPath = path; - break; - case Wizard::Component_Tribunal: - mTribunalPath = path; - break; - case Wizard::Component_Bloodmoon: - mBloodmoonPath = path; - break; - } - - mWait.wakeAll(); -} - -QString Wizard::UnshieldWorker::getComponentPath(Component component) -{ - QReadLocker readLock(&mLock); - switch (component) { - - case Wizard::Component_Morrowind: - return mMorrowindPath; - case Wizard::Component_Tribunal: - return mTribunalPath; - case Wizard::Component_Bloodmoon: - return mBloodmoonPath; - } - - return QString(); -} - void Wizard::UnshieldWorker::setComponentDone(Component component, bool done) { QWriteLocker writeLock(&mLock); @@ -159,6 +121,13 @@ void Wizard::UnshieldWorker::setIniPath(const QString &path) mIniPath = path; } +void Wizard::UnshieldWorker::setDiskPath(const QString &path) +{ + QWriteLocker writeLock(&mLock); + mDiskPath = path; + mWait.wakeAll(); +} + QString Wizard::UnshieldWorker::getPath() { QReadLocker readLock(&mLock); @@ -171,6 +140,13 @@ QString Wizard::UnshieldWorker::getIniPath() return mIniPath; } +QString Wizard::UnshieldWorker::getDiskPath() +{ + QReadLocker readLock(&mLock); + return mDiskPath; +} + + void Wizard::UnshieldWorker::setIniCodec(QTextCodec *codec) { QWriteLocker writeLock(&mLock); @@ -332,7 +308,7 @@ bool Wizard::UnshieldWorker::installFile(const QString &fileName, const QString QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst)); - foreach(QFileInfo info, list) { + foreach(const QFileInfo &info, list) { if (info.isDir()) { result = installFile(fileName, info.absoluteFilePath()); } else { @@ -360,7 +336,7 @@ bool Wizard::UnshieldWorker::installDirectory(const QString &dirName, const QStr QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs)); - foreach(QFileInfo info, list) { + foreach(const QFileInfo &info, list) { if (info.isSymLink()) continue; @@ -383,190 +359,75 @@ void Wizard::UnshieldWorker::extract() { qDebug() << "extract!"; - qDebug() << findFiles(QLatin1String("data1.hdr"), QLatin1String("/mnt/cdrom")); -// QDir disk; - -// if (getInstallComponent(Wizard::Component_Morrowind)) -// { -// if (!getComponentDone(Wizard::Component_Morrowind)) -// { -// if (getComponentPath(Wizard::Component_Morrowind).isEmpty()) { -// qDebug() << "request file dialog"; -// QReadLocker readLock(&mLock); -// emit requestFileDialog(Wizard::Component_Morrowind); -// mWait.wait(&mLock); -// } - -// if (!getComponentPath(Wizard::Component_Morrowind).isEmpty()) { -// disk.setPath(getComponentPath(Wizard::Component_Morrowind)); - -// if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), QLatin1String("Morrowind.bsa"))) -// { -// QReadLocker readLock(&mLock); -// emit requestFileDialog(Wizard::Component_Morrowind); -// mWait.wait(&mLock); -// } else { -// if (installComponent(Wizard::Component_Morrowind)) { -// setComponentDone(Wizard::Component_Morrowind, true); -// } else { -// qDebug() << "Erorr installing Morrowind"; - -// return; -// } -// } -// } -// } -// } - -// if (getInstallComponent(Wizard::Component_Tribunal)) -// { -// setupAddon(Wizard::Component_Tribunal); -// } - -// if (getInstallComponent(Wizard::Component_Bloodmoon)) -// { -// setupAddon(Wizard::Component_Bloodmoon); -// } - -// // Update Morrowind configuration -// if (getInstallComponent(Wizard::Component_Tribunal)) -// { -// mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); -// mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); -// } - -// if (getInstallComponent(Wizard::Component_Bloodmoon)) -// { -// mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Bloodmoon.bsa"))); -// mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Bloodmoon.esm"))); -// } - -// if (getInstallComponent(Wizard::Component_Tribunal) && -// getInstallComponent(Wizard::Component_Bloodmoon)) -// { -// mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); -// mIniSettings.setValue(QLatin1String("Archives/Archive1"), QVariant(QString("Bloodmoon.bsa"))); -// mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); -// mIniSettings.setValue(QLatin1String("Game Files/Game File2"), QVariant(QString("Bloodmoon.esm"))); -// } - - -// // Write the settings to the Morrowind config file -// writeSettings(); - -// // Remove the temporary directory -// //removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); - -// // Fill the progress bar -// int total = 0; - -// if (getInstallComponent(Wizard::Component_Morrowind)) -// total = 100; - -// if (getInstallComponent(Wizard::Component_Tribunal)) -// total = total + 100; - -// if (getInstallComponent(Wizard::Component_Bloodmoon)) -// total = total + 100; - -// emit textChanged(tr("Installation finished!")); -// emit progressChanged(total); -// emit finished(); - -// qDebug() << "installation finished!"; -} - -void Wizard::UnshieldWorker::setupAddon(Component component) -{ - qDebug() << "SetupAddon!" << getComponentPath(component) << getComponentPath(Wizard::Component_Morrowind); - - if (!getComponentDone(component)) + if (getInstallComponent(Wizard::Component_Morrowind)) { - qDebug() << "Component not done!"; - - QDir disk(getComponentPath(Wizard::Component_Morrowind)); - QString name; - if (component == Wizard::Component_Tribunal) - name = QLatin1String("Tribunal"); - - if (component == Wizard::Component_Bloodmoon) - name = QLatin1String("Bloodmoon"); - - if (name.isEmpty()) { - emit error(tr("Component parameter is invalid!"), tr("An invalid component parameter was supplied.")); - return; - } - - qDebug() << "Determine if file is in current data1.hdr: " << name; - - if (!disk.isEmpty()) { - if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) - { - if (!disk.cd(name)) { - qDebug() << "not found on cd!"; - QReadLocker locker(&mLock); - emit requestFileDialog(component); - mWait.wait(&mLock); - - } else if (disk.exists(QLatin1String("data1.hdr"))) { - if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) - { - QReadLocker locker(&mLock); - emit requestFileDialog(component); - mWait.wait(&mLock); - } else { - setComponentPath(component, disk.absolutePath()); - disk.setPath(getComponentPath(component)); - } - } - } - - } else { - QReadLocker locker(&mLock); - emit requestFileDialog(component); - mWait.wait(&mLock); - } - - disk.setPath(getComponentPath(component)); - - if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) - { - if (!disk.cd(name)) { - qDebug() << "not found on cd!"; - QReadLocker locker(&mLock); - emit requestFileDialog(component); - mWait.wait(&mLock); - - } else if (disk.exists(QLatin1String("data1.hdr"))) { - if (!findInCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), name + QLatin1String(".bsa"))) - { - QReadLocker locker(&mLock); - emit requestFileDialog(component); - mWait.wait(&mLock); - } else { - setComponentPath(component, disk.absolutePath()); - disk.setPath(getComponentPath(component)); - } - } - - // Make sure the dir is up-to-date - //disk.setPath(getComponentPath(component)); - } - - // Now do the actual installing - - if (installComponent(component)) { - setComponentDone(component, true); - } else { - qDebug() << "Error installing " << name; - return; - } - - + if (!getComponentDone(Wizard::Component_Morrowind)) + if (!setupComponent(Wizard::Component_Morrowind)) + return; } + + if (getInstallComponent(Wizard::Component_Tribunal)) + { + if (!getComponentDone(Wizard::Component_Tribunal)) + if (!setupComponent(Wizard::Component_Tribunal)) + return; + } + + if (getInstallComponent(Wizard::Component_Bloodmoon)) + { + if (!getComponentDone(Wizard::Component_Bloodmoon)) + if (!setupComponent(Wizard::Component_Bloodmoon)) + return; + } + + // Update Morrowind configuration + if (getInstallComponent(Wizard::Component_Tribunal)) + { + mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); + } + + if (getInstallComponent(Wizard::Component_Bloodmoon)) + { + mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Bloodmoon.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Bloodmoon.esm"))); + } + + if (getInstallComponent(Wizard::Component_Tribunal) && + getInstallComponent(Wizard::Component_Bloodmoon)) + { + mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); + mIniSettings.setValue(QLatin1String("Archives/Archive1"), QVariant(QString("Bloodmoon.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); + mIniSettings.setValue(QLatin1String("Game Files/Game File2"), QVariant(QString("Bloodmoon.esm"))); + } + + // Write the settings to the Morrowind config file + writeSettings(); + + // Remove the temporary directory + removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); + + // Fill the progress bar + int total = 0; + + if (getInstallComponent(Wizard::Component_Morrowind)) + total = 100; + + if (getInstallComponent(Wizard::Component_Tribunal)) + total = total + 100; + + if (getInstallComponent(Wizard::Component_Bloodmoon)) + total = total + 100; + + emit textChanged(tr("Installation finished!")); + emit progressChanged(total); + emit finished(); + + qDebug() << "installation finished!"; } -bool Wizard::UnshieldWorker::installComponent(Component component) +bool Wizard::UnshieldWorker::setupComponent(Component component) { QString name; switch (component) { @@ -587,13 +448,79 @@ bool Wizard::UnshieldWorker::installComponent(Component component) return false; } + bool found = false; + QString cabFile; + QDir disk; + + // Keep showing the file dialog until we find the necessary install files + while (!found) { + if (getDiskPath().isEmpty()) { + QReadLocker readLock(&mLock); + emit requestFileDialog(component); + mWait.wait(&mLock); + disk.setPath(getDiskPath()); + } else { + disk.setPath(getDiskPath()); + } + + QStringList list(findFiles(QLatin1String("data1.hdr"), disk.absolutePath())); + + foreach (const QString &file, list) { + qDebug() << "current cab file is: " << file; + if (findInCab(file, name + QLatin1String(".bsa"))) { + cabFile = file; + found = true; + } + } + + if (!found) { + QReadLocker readLock(&mLock); + emit requestFileDialog(component); + mWait.wait(&mLock); + } + } + + if (installComponent(component, cabFile)) { + setComponentDone(component, true); + return true; + } else { + qDebug() << "Erorr installing " << name; + return false; + } + + return true; +} + +bool Wizard::UnshieldWorker::installComponent(Component component, const QString &path) +{ + QString name; + switch (component) { + + case Wizard::Component_Morrowind: + name = QLatin1String("Morrowind"); + break; + case Wizard::Component_Tribunal: + name = QLatin1String("Tribunal"); + break; + case Wizard::Component_Bloodmoon: + name = QLatin1String("Bloodmoon"); + break; + } + + if (name.isEmpty()) { + emit error(tr("Component parameter is invalid!"), tr("An invalid component parameter was supplied.")); + return false; + } + + qDebug() << "Install " << name << " from " << path; + + emit textChanged(tr("Installing %1").arg(name)); - QDir disk(getComponentPath(component)); + QFileInfo info(path); - if (!disk.exists()) { - qDebug() << "Component path not set: " << getComponentPath(Wizard::Component_Morrowind); - emit error(tr("Component path not set!"), tr("The source path for %1 was not set.").arg(name)); + if (!info.exists()) { + emit error(tr("Installation media path not set!"), tr("The source path for %1 was not set.").arg(name)); return false; } @@ -606,7 +533,6 @@ bool Wizard::UnshieldWorker::installComponent(Component component) removeDirectory(tempPath); if (!temp.mkpath(tempPath)) { - qDebug() << "Can't make path"; emit error(tr("Cannot create temporary directory!"), tr("Failed to create %1.").arg(tempPath)); return false; } @@ -619,24 +545,15 @@ bool Wizard::UnshieldWorker::installComponent(Component component) } if (!temp.cd(name)) { - qDebug() << "Can't cd to dir"; emit error(tr("Cannot move into temporary directory!"), tr("Failed to move into %1.").arg(temp.absoluteFilePath(name))); return false; } // Extract the installation files - if (!extractCab(disk.absoluteFilePath(QLatin1String("data1.hdr")), temp.absolutePath())) + if (!extractCab(info.absoluteFilePath(), temp.absolutePath())) return false; // Move the files from the temporary path to the destination folder -// emit textChanged(tr("Moving installation files")); -// if (!moveDirectory(temp.absoluteFilePath(QLatin1String("Data Files")), getPath())) { -// qDebug() << "failed to move files!"; -// emit error(tr("Moving extracted files failed!"), -// tr("Failed to move files from %1 to %2.").arg(temp.absoluteFilePath(QLatin1String("Data Files")), -// getPath())); -// return false; -// } emit textChanged(tr("Moving installation files")); // Install extracted directories @@ -658,20 +575,20 @@ bool Wizard::UnshieldWorker::installComponent(Component component) // Install directories from disk foreach (const QString &dir, directories) { qDebug() << "\n\nDISK DIRS!"; - installDirectory(dir, disk.absolutePath(), false); + installDirectory(dir, info.absolutePath(), false); } - QFileInfo info(disk.absoluteFilePath("Data Files")); - if (info.exists()) { + QFileInfo datafiles(info.absolutePath() + QDir::separator() + QLatin1String("Data Files")); + if (datafiles.exists()) { emit textChanged(tr("Installing: Data Files directory")); - copyDirectory(info.absoluteFilePath(), getPath()); + copyDirectory(datafiles.absoluteFilePath(), getPath()); } if (component == Wizard::Component_Morrowind) { QStringList files; files << QLatin1String("Morrowind.esm") - << QLatin1String("Morrowin.bsa"); + << QLatin1String("Morrowind.bsa"); foreach (const QString &file, files) { if (!installFile(file, temp.absolutePath())) { @@ -702,7 +619,9 @@ bool Wizard::UnshieldWorker::installComponent(Component component) QStringList files; files << QLatin1String("Tribunal.esm") - << QLatin1String("Tribunal.bsa"); + << QLatin1String("Tribunal.bsa") + << QLatin1String("Morrowind.esm") + << QLatin1String("Morrowind.bsa"); foreach (const QString &file, files) { if (!installFile(file, temp.absolutePath())) { @@ -710,7 +629,6 @@ bool Wizard::UnshieldWorker::installComponent(Component component) return false; } } - } if (component == Wizard::Component_Bloodmoon) @@ -726,7 +644,9 @@ bool Wizard::UnshieldWorker::installComponent(Component component) QStringList files; files << QLatin1String("Bloodmoon.esm") - << QLatin1String("Bloodmoon.bsa"); + << QLatin1String("Bloodmoon.bsa") + << QLatin1String("Morrowind.esm") + << QLatin1String("Morrowind.bsa"); foreach (const QString &file, files) { if (!installFile(file, temp.absolutePath())) { @@ -736,13 +656,12 @@ bool Wizard::UnshieldWorker::installComponent(Component component) } // Load Morrowind configuration settings from the setup script - QFileInfo inx(disk.absoluteFilePath(QLatin1String("setup.inx"))); + QStringList list(findFiles(QLatin1String("setup.inx"), getDiskPath())); - if (inx.exists()) { - emit textChanged(tr("Updating Morrowind configuration file")); - mIniSettings.parseInx(inx.absoluteFilePath()); - } else { - qDebug() << "setup.inx not found!"; + emit textChanged(tr("Updating Morrowind configuration file")); + + foreach (const QString &inx, list) { + mIniSettings.parseInx(inx); } } @@ -799,15 +718,23 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &dest return success; } -QString Wizard::UnshieldWorker::findFile(const QString &fileName, const QString &path) +QString Wizard::UnshieldWorker::findFile(const QString &fileName, const QString &path, int depth) { - return findFiles(fileName, path).first(); + return findFiles(fileName, path, depth).first(); } -QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QString &path) +QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QString &path, int depth) { + qDebug() << "Searching path: " << path << " for: " << fileName; + static const int MAXIMUM_DEPTH = 5; + + if (depth >= MAXIMUM_DEPTH) { + qWarning("Maximum directory depth limit reached."); + return QStringList(); + } + QStringList result; - QDir dir(source); + QDir dir(path); if (!dir.exists()) return QStringList(); @@ -821,7 +748,7 @@ QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QSt continue; if (info.isDir()) { - result = findFiles(file, info.absoluteFilePath()); + result.append(findFiles(fileName, info.absoluteFilePath(), depth + 1)); } else { if (info.fileName() == fileName) { qDebug() << "File found at: " << info.absoluteFilePath(); @@ -831,7 +758,6 @@ QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QSt } return result; - } bool Wizard::UnshieldWorker::findInCab(const QString &cabFile, const QString &fileName) @@ -855,7 +781,6 @@ bool Wizard::UnshieldWorker::findInCab(const QString &cabFile, const QString &fi if (unshield_file_is_valid(unshield, j)) { QString current(QString::fromUtf8(unshield_file_name(unshield, j))); - qDebug() << "Current is: " << current; if (current.toLower() == fileName.toLower()) return true; // File is found! } diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index 74ae6c6d85..bda9514568 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -31,7 +31,8 @@ namespace Wizard void setInstallComponent(Wizard::Component component, bool install); - void setComponentPath(Wizard::Component component, const QString &path); +// void setComponentPath(Wizard::Component component, const QString &path); + void setDiskPath(const QString &path); void setPath(const QString &path); void setIniPath(const QString &path); @@ -49,7 +50,8 @@ namespace Wizard bool getInstallComponent(Component component); - QString getComponentPath(Component component); + //QString getComponentPath(Component component); + QString getDiskPath(); void setComponentDone(Component component, bool done = true); bool getComponentDone(Component component); @@ -66,18 +68,14 @@ namespace Wizard bool extractFile(Unshield *unshield, const QString &destination, const QString &prefix, int index, int counter); bool findInCab(const QString &cabFile, const QString &fileName); - QString findFile(const QString &fileName, const QString &path); - QStringList findFiles(const QString &fileName, const QString &path); + QString findFile(const QString &fileName, const QString &path, int depth = 0); + QStringList findFiles(const QString &fileName, const QString &path, int depth = 0); bool installFile(const QString &fileName, const QString &path); bool installDirectory(const QString &dirName, const QString &path, bool recursive = true); - bool installMorrowind(); - bool installTribunal(); - bool installBloodmoon(); - - bool installComponent(Component component); - void setupAddon(Component component); + bool installComponent(Component component, const QString &path); + bool setupComponent(Component component); bool mInstallMorrowind; bool mInstallTribunal; @@ -87,12 +85,9 @@ namespace Wizard bool mTribunalDone; bool mBloodmoonDone; - QString mMorrowindPath; - QString mTribunalPath; - QString mBloodmoonPath; - QString mPath; QString mIniPath; + QString mDiskPath; IniSettings mIniSettings; From 254fe0a424edee364030970af3c4f4c8a8e19ee5 Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 17 Mar 2014 14:31:05 +0100 Subject: [PATCH 039/303] Made install functions use file finding methods and improved error handling --- apps/wizard/conclusionpage.cpp | 2 +- apps/wizard/installationpage.cpp | 30 +-- apps/wizard/mainwizard.cpp | 3 +- apps/wizard/unshield/unshieldworker.cpp | 266 ++++++++++++++---------- apps/wizard/unshield/unshieldworker.hpp | 25 ++- 5 files changed, 184 insertions(+), 142 deletions(-) diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index 3681459b72..7b638813b7 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -22,7 +22,7 @@ void Wizard::ConclusionPage::initializePage() if (!mWizard->mError) { if ((field(QLatin1String("installation.new")).toBool() == true) - | (field(QLatin1String("installation.import-settings")).toBool() == true)) + || (field(QLatin1String("installation.import-settings")).toBool() == true)) { qDebug() << "IMPORT SETTINGS"; mWizard->runSettingsImporter(); diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 3fcbd1fe29..74666c394c 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -44,8 +44,6 @@ void Wizard::InstallationPage::initializePage() installProgressBar->setMaximum(installProgressBar->maximum() + 100); } - // Handle when cancel is clicked - startInstallation(); } @@ -54,21 +52,21 @@ void Wizard::InstallationPage::startInstallation() QStringList components(field(QLatin1String("installation.components")).toStringList()); QString path(field(QLatin1String("installation.path")).toString()); - QThread *thread = new QThread(); + mThread = new QThread(); mUnshield = new UnshieldWorker(); - mUnshield->moveToThread(thread); + mUnshield->moveToThread(mThread); - connect(thread, SIGNAL(started()), + connect(mThread, SIGNAL(started()), mUnshield, SLOT(extract())); connect(mUnshield, SIGNAL(finished()), - thread, SLOT(quit())); + mThread, SLOT(quit())); connect(mUnshield, SIGNAL(finished()), mUnshield, SLOT(deleteLater())); connect(mUnshield, SIGNAL(finished()), - thread, SLOT(deleteLater())); + mThread, SLOT(deleteLater()));; connect(mUnshield, SIGNAL(finished()), this, SLOT(installationFinished()), Qt::QueuedConnection); @@ -129,7 +127,7 @@ void Wizard::InstallationPage::startInstallation() mUnshield->setIniCodec(QTextCodec::codecForName("windows-1252")); } - thread->start(); + mThread->start(); } void Wizard::InstallationPage::showFileDialog(Wizard::Component component) @@ -148,23 +146,15 @@ void Wizard::InstallationPage::showFileDialog(Wizard::Component component) break; } -// QString fileName = QFileDialog::getOpenFileName( -// this, -// tr("Select %1 installation file").arg(name), -// QDir::rootPath(), -// tr("InstallShield header files (*.hdr)")); - - QString path = QFileDialog::getExistingDirectory(this, tr("Select %1 installation media").arg(name), QDir::rootPath()); if (path.isEmpty()) { - qDebug() << "Cancel was clicked!"; - logTextEdit->appendHtml(tr("


\ Error: The installation was aborted by the user

")); mWizard->mError = true; + emit completeChanged(); return; } @@ -174,8 +164,6 @@ void Wizard::InstallationPage::showFileDialog(Wizard::Component component) void Wizard::InstallationPage::installationFinished() { - qDebug() << "finished!"; - QMessageBox msgBox; msgBox.setWindowTitle(tr("Installation finished")); msgBox.setIcon(QMessageBox::Information); @@ -186,13 +174,10 @@ void Wizard::InstallationPage::installationFinished() mFinished = true; emit completeChanged(); - } void Wizard::InstallationPage::installationError(const QString &text, const QString &details) { - qDebug() << "error: " << text; - installProgressLabel->setText(tr("Installation failed!")); logTextEdit->appendHtml(tr("


\ @@ -213,7 +198,6 @@ void Wizard::InstallationPage::installationError(const QString &text, const QStr mWizard->mError = true; emit completeChanged(); - } bool Wizard::InstallationPage::isComplete() const diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index e6dfc67d1d..2fec3cec38 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -143,7 +143,8 @@ void Wizard::MainWizard::runSettingsImporter() QStringList arguments; // Import plugin selection? - if (field(QLatin1String("installation.import-addons")).toBool() == true) + if (field(QLatin1String("installation.new")).toBool() == true + || field(QLatin1String("installation.import-addons")).toBool() == true) arguments.append(QLatin1String("--game-files")); arguments.append(QLatin1String("--encoding")); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 223de41ce4..2319ca0df5 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -162,8 +162,8 @@ void Wizard::UnshieldWorker::setupSettings() QFile file(getIniPath()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - qDebug() << "Error opening .ini file!"; - emit error(tr("Failed to open Morrowind configuration file!"), tr("Opening %1 failed: %2.").arg(getIniPath(), file.errorString())); + emit error(tr("Failed to open Morrowind configuration file!"), + tr("Opening %1 failed: %2.").arg(getIniPath(), file.errorString())); return; } @@ -181,7 +181,6 @@ void Wizard::UnshieldWorker::writeSettings() QFile file(getIniPath()); if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) { - qDebug() << "Error opening .ini file!"; emit error(tr("Failed to open Morrowind configuration file!"), tr("Opening %1 failed: %2.").arg(getIniPath(), file.errorString())); return; @@ -198,7 +197,7 @@ void Wizard::UnshieldWorker::writeSettings() bool Wizard::UnshieldWorker::removeDirectory(const QString &dirName) { - bool result = true; + bool result = false; QDir dir(dirName); if (dir.exists(dirName)) @@ -229,38 +228,42 @@ bool Wizard::UnshieldWorker::copyFile(const QString &source, const QString &dest QFileInfo info(destination); - if (info.exists()) - dir.remove(info.absoluteFilePath()); + if (info.exists()) { + if (!dir.remove(info.absoluteFilePath())) + return false; + } if (file.copy(source, destination)) { if (!keepSource) { - return file.remove(source); + if (!file.remove(source)) + return false; } else { return true; } } else { - qDebug() << "copy failed! " << file.errorString(); - emit error(tr("Failed to copy file!"), tr("Copying %1 to %2 failed: %3.").arg(source, destination, file.errorString())); + return false; } - return false; + return true; } bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString &destination, bool keepSource) { QDir sourceDir(source); QDir destDir(destination); + bool result = false; if (!destDir.exists()) { - sourceDir.mkpath(destination); + if (!sourceDir.mkpath(destination)) { + return false; + } + destDir.refresh(); } if (!destDir.exists()) return false; - bool result = true; - QFileInfoList list(sourceDir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst)); @@ -269,13 +272,15 @@ bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString QString relativePath(info.absoluteFilePath()); relativePath.remove(source); + QString destinationPath(destDir.absolutePath() + relativePath); + if (info.isSymLink()) continue; if (info.isDir()) { - result = moveDirectory(info.absoluteFilePath(), destDir.absolutePath() + relativePath); + result = copyDirectory(info.absoluteFilePath(), destinationPath); } else { - result = moveFile(info.absoluteFilePath(), destDir.absolutePath() + relativePath); + result = copyFile(info.absoluteFilePath(), destinationPath); } } @@ -285,80 +290,56 @@ bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString return result; } -bool Wizard::UnshieldWorker::moveFile(const QString &source, const QString &destination) +bool Wizard::UnshieldWorker::installFile(const QString &fileName, const QString &path, Qt::MatchFlags flags, bool keepSource) { - return copyFile(source, destination, false); + return installFiles(fileName, path, flags, true, keepSource); } -bool Wizard::UnshieldWorker::moveDirectory(const QString &source, const QString &destination) +bool Wizard::UnshieldWorker::installFiles(const QString &fileName, const QString &path, Qt::MatchFlags flags, bool keepSource, bool single) { - return copyDirectory(source, destination, false); -} - -bool Wizard::UnshieldWorker::installFile(const QString &fileName, const QString &path) -{ - qDebug() << "Attempting to find file: " << fileName << " in: " << path; - - bool result = true; QDir dir(path); if (!dir.exists()) return false; - QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | - QDir::System | QDir::Hidden | - QDir::AllDirs | QDir::Files, QDir::DirsFirst)); - foreach(const QFileInfo &info, list) { - if (info.isDir()) { - result = installFile(fileName, info.absoluteFilePath()); + QStringList files(findFiles(fileName, path, flags)); + + foreach (const QString &file, files) { + QFileInfo info(file); + emit textChanged(tr("Installing: %1").arg(info.fileName())); + + if (single) { + return copyFile(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName(), keepSource); } else { - if (info.fileName() == fileName) { - qDebug() << "File found at: " << info.absoluteFilePath(); - - emit textChanged(tr("Installing: %1").arg(info.fileName())); - return moveFile(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName()); - } + if (!copyFile(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName(), keepSource)) + return false; } } - return result; + return true; } -bool Wizard::UnshieldWorker::installDirectory(const QString &dirName, const QString &path, bool recursive) +bool Wizard::UnshieldWorker::installDirectories(const QString &dirName, const QString &path, bool recursive, bool keepSource) { - qDebug() << "Attempting to find: " << dirName << " in: " << path; - bool result = true; QDir dir(path); if (!dir.exists()) return false; - QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | - QDir::System | QDir::Hidden | - QDir::AllDirs)); - foreach(const QFileInfo &info, list) { - if (info.isSymLink()) - continue; + QStringList directories(findDirectories(dirName, path, recursive)); - if (info.isDir()) { - if (info.fileName() == dirName) { - qDebug() << "Directory found at: " << info.absoluteFilePath(); - emit textChanged(tr("Installing: %1 directory").arg(info.fileName())); - return copyDirectory(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName()); - } else { - if (recursive) - result = installDirectory(dirName, info.absoluteFilePath()); - } - } + foreach (const QString &dir, directories) { + QFileInfo info(dir); + emit textChanged(tr("Installing: %1 directory").arg(info.fileName())); + if (!copyDirectory(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName(), keepSource)) + return false; } - return result; + return true; } void Wizard::UnshieldWorker::extract() { - qDebug() << "extract!"; - if (getInstallComponent(Wizard::Component_Morrowind)) { if (!getComponentDone(Wizard::Component_Morrowind)) @@ -383,23 +364,23 @@ void Wizard::UnshieldWorker::extract() // Update Morrowind configuration if (getInstallComponent(Wizard::Component_Tribunal)) { - mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); - mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); + mIniSettings.setValue(QLatin1String("Archives/Archive 0"), QVariant(QString("Tribunal.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/GameFile1"), QVariant(QString("Tribunal.esm"))); } if (getInstallComponent(Wizard::Component_Bloodmoon)) { - mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Bloodmoon.bsa"))); - mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Bloodmoon.esm"))); + mIniSettings.setValue(QLatin1String("Archives/Archive 0"), QVariant(QString("Bloodmoon.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/GameFile1"), QVariant(QString("Bloodmoon.esm"))); } if (getInstallComponent(Wizard::Component_Tribunal) && getInstallComponent(Wizard::Component_Bloodmoon)) { - mIniSettings.setValue(QLatin1String("Archives/Archive0"), QVariant(QString("Tribunal.bsa"))); - mIniSettings.setValue(QLatin1String("Archives/Archive1"), QVariant(QString("Bloodmoon.bsa"))); - mIniSettings.setValue(QLatin1String("Game Files/Game File1"), QVariant(QString("Tribunal.esm"))); - mIniSettings.setValue(QLatin1String("Game Files/Game File2"), QVariant(QString("Bloodmoon.esm"))); + mIniSettings.setValue(QLatin1String("Archives/Archive 0"), QVariant(QString("Tribunal.bsa"))); + mIniSettings.setValue(QLatin1String("Archives/Archive 1"), QVariant(QString("Bloodmoon.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/GameFile1"), QVariant(QString("Tribunal.esm"))); + mIniSettings.setValue(QLatin1String("Game Files/GameFile2"), QVariant(QString("Bloodmoon.esm"))); } // Write the settings to the Morrowind config file @@ -423,8 +404,6 @@ void Wizard::UnshieldWorker::extract() emit textChanged(tr("Installation finished!")); emit progressChanged(total); emit finished(); - - qDebug() << "installation finished!"; } bool Wizard::UnshieldWorker::setupComponent(Component component) @@ -466,10 +445,26 @@ bool Wizard::UnshieldWorker::setupComponent(Component component) QStringList list(findFiles(QLatin1String("data1.hdr"), disk.absolutePath())); foreach (const QString &file, list) { - qDebug() << "current cab file is: " << file; - if (findInCab(file, name + QLatin1String(".bsa"))) { - cabFile = file; - found = true; + + if (component == Wizard::Component_Morrowind) + { + bool morrowindFound = findInCab(file, QLatin1String("Morrowind.bsa")); + bool tribunalFound = findInCab(file, QLatin1String("Tribunal.bsa")); + bool bloodmoonFound = findInCab(file, QLatin1String("Bloodmoon.bsa")); + + if (morrowindFound) { + // Check if we have correct archive, other archives have Morrowind.bsa too + if ((tribunalFound && bloodmoonFound) + || (!tribunalFound && !bloodmoonFound)) { + cabFile = file; + found = true; // We have a GoTY disk + } + } + } else { + if (findInCab(file, name + QLatin1String(".bsa"))) { + cabFile = file; + found = true; + } } } @@ -484,7 +479,6 @@ bool Wizard::UnshieldWorker::setupComponent(Component component) setComponentDone(component, true); return true; } else { - qDebug() << "Erorr installing " << name; return false; } @@ -512,9 +506,6 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString return false; } - qDebug() << "Install " << name << " from " << path; - - emit textChanged(tr("Installing %1").arg(name)); QFileInfo info(path); @@ -569,19 +560,34 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString << QLatin1String("Video"); foreach (const QString &dir, directories) { - installDirectory(dir, temp.absolutePath()); + if (!installDirectories(dir, temp.absolutePath())) { + emit error(tr("Could not install directory!"), + tr("Installing %1 to %2 failed.").arg(dir, temp.absolutePath())); + return false; + } } // Install directories from disk foreach (const QString &dir, directories) { - qDebug() << "\n\nDISK DIRS!"; - installDirectory(dir, info.absolutePath(), false); + if (!installDirectories(dir, info.absolutePath(), false, true)) { + emit error(tr("Could not install directory!"), + tr("Installing %1 to %2 failed.").arg(dir, info.absolutePath())); + } + } - QFileInfo datafiles(info.absolutePath() + QDir::separator() + QLatin1String("Data Files")); - if (datafiles.exists()) { - emit textChanged(tr("Installing: Data Files directory")); - copyDirectory(datafiles.absoluteFilePath(), getPath()); + // Install translation files + QStringList extensions; + extensions << QLatin1String(".cel") + << QLatin1String(".top") + << QLatin1String(".mrk"); + + foreach (const QString &extension, extensions) { + if (!installFiles(extension, info.absolutePath(), Qt::MatchEndsWith)) { + emit error(tr("Could not install translation file!"), + tr("Failed to install *%1 files.").arg(extension)); + return false; + } } if (component == Wizard::Component_Morrowind) @@ -592,14 +598,16 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString foreach (const QString &file, files) { if (!installFile(file, temp.absolutePath())) { - emit error(tr("Could not find Morrowind data file!"), tr("Failed to find %1.").arg(file)); + emit error(tr("Could not install Morrowind data file!"), + tr("Failed to install %1.").arg(file)); return false; } } // Copy Morrowind configuration file if (!installFile(QLatin1String("Morrowind.ini"), temp.absolutePath())) { - emit error(tr("Could not find Morrowind configuration file!"), tr("Failed to find %1.").arg(QLatin1String("Morrowind.ini"))); + emit error(tr("Could not install Morrowind configuration file!"), + tr("Failed to install %1.").arg(QLatin1String("Morrowind.ini"))); return false; } @@ -611,21 +619,25 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString if (component == Wizard::Component_Tribunal) { QFileInfo sounds(temp.absoluteFilePath(QLatin1String("Sounds"))); + QString dest(getPath() + QDir::separator() + QLatin1String("Sound")); if (sounds.exists()) { emit textChanged(tr("Installing: Sound directory")); - copyDirectory(sounds.absoluteFilePath(), getPath() + QDir::separator() + QLatin1String("Sound")); + if (!copyDirectory(sounds.absoluteFilePath(), dest)) { + emit error(tr("Could not install directory!"), + tr("Installing %1 to %2 failed.").arg(sounds.absoluteFilePath(), dest)); + } + } QStringList files; files << QLatin1String("Tribunal.esm") - << QLatin1String("Tribunal.bsa") - << QLatin1String("Morrowind.esm") - << QLatin1String("Morrowind.bsa"); + << QLatin1String("Tribunal.bsa"); foreach (const QString &file, files) { if (!installFile(file, temp.absolutePath())) { - emit error(tr("Could not find Tribunal data file!"), tr("Failed to find %1.").arg(file)); + emit error(tr("Could not find Tribunal data file!"), + tr("Failed to find %1.").arg(file)); return false; } } @@ -637,20 +649,20 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString if (original.exists()) { if (!installFile(QLatin1String("Tribunal.esm"), temp.absolutePath())) { - emit error(tr("Could not find Tribunal patch file!"), tr("Failed to find %1.").arg(QLatin1String("Tribunal.esm"))); + emit error(tr("Could not find Tribunal patch file!"), + tr("Failed to find %1.").arg(QLatin1String("Tribunal.esm"))); return false; } } QStringList files; files << QLatin1String("Bloodmoon.esm") - << QLatin1String("Bloodmoon.bsa") - << QLatin1String("Morrowind.esm") - << QLatin1String("Morrowind.bsa"); + << QLatin1String("Bloodmoon.bsa"); foreach (const QString &file, files) { if (!installFile(file, temp.absolutePath())) { - emit error(tr("Could not find Bloodmoon data file!"), tr("Failed to find %1.").arg(file)); + emit error(tr("Could not find Bloodmoon data file!"), + tr("Failed to find %1.").arg(file)); return false; } } @@ -665,6 +677,21 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString } } + // Finally, install Data Files directories from temp and disk + QStringList datafiles(findDirectories(QLatin1String("Data Files"), temp.absolutePath())); + datafiles.append(findDirectories(QLatin1String("Data Files"), info.absolutePath())); + + foreach (const QString &dir, datafiles) { + QFileInfo info(dir); + emit textChanged(tr("Installing: %1 directory").arg(info.fileName())); + + if (!copyDirectory(info.absoluteFilePath(), getPath())) { + emit error(tr("Could not install directory!"), + tr("Installing %1 to %2 failed.").arg(info.absoluteFilePath(), getPath())); + return false; + } + } + emit textChanged(tr("%1 installation finished!").arg(name)); return true; @@ -718,15 +745,15 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &dest return success; } -QString Wizard::UnshieldWorker::findFile(const QString &fileName, const QString &path, int depth) +QString Wizard::UnshieldWorker::findFile(const QString &fileName, const QString &path) { - return findFiles(fileName, path, depth).first(); + return findFiles(fileName, path).first(); } -QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QString &path, int depth) +QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QString &path, int depth, bool recursive, + bool directories, Qt::MatchFlags flags) { - qDebug() << "Searching path: " << path << " for: " << fileName; - static const int MAXIMUM_DEPTH = 5; + static const int MAXIMUM_DEPTH = 10; if (depth >= MAXIMUM_DEPTH) { qWarning("Maximum directory depth limit reached."); @@ -748,11 +775,31 @@ QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QSt continue; if (info.isDir()) { - result.append(findFiles(fileName, info.absoluteFilePath(), depth + 1)); + if (directories) + { + if (info.fileName() == fileName) { + result.append(info.absoluteFilePath()); + } else { + if (recursive) + result.append(findFiles(fileName, info.absoluteFilePath(), depth + 1, recursive, true)); + } + } else { + if (recursive) + result.append(findFiles(fileName, info.absoluteFilePath(), depth + 1)); + } } else { - if (info.fileName() == fileName) { - qDebug() << "File found at: " << info.absoluteFilePath(); - result.append(info.absoluteFilePath()); + if (directories) + break; + + switch (flags) { + case Qt::MatchExactly: + if (info.fileName() == fileName) + result.append(info.absoluteFilePath()); + break; + case Qt::MatchEndsWith: + if (info.fileName().endsWith(fileName)) + result.append(info.absoluteFilePath()); + break; } } } @@ -760,6 +807,11 @@ QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QSt return result; } +QStringList Wizard::UnshieldWorker::findDirectories(const QString &dirName, const QString &path, bool recursive) +{ + return findFiles(dirName, path, 0, true, true); +} + bool Wizard::UnshieldWorker::findInCab(const QString &cabFile, const QString &fileName) { QByteArray array(cabFile.toUtf8()); diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index bda9514568..f23dc1aa76 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -31,7 +32,6 @@ namespace Wizard void setInstallComponent(Wizard::Component component, bool install); -// void setComponentPath(Wizard::Component component, const QString &path); void setDiskPath(const QString &path); void setPath(const QString &path); @@ -50,7 +50,6 @@ namespace Wizard bool getInstallComponent(Component component); - //QString getComponentPath(Component component); QString getDiskPath(); void setComponentDone(Component component, bool done = true); @@ -61,18 +60,25 @@ namespace Wizard bool copyFile(const QString &source, const QString &destination, bool keepSource = true); bool copyDirectory(const QString &source, const QString &destination, bool keepSource = true); - bool moveFile(const QString &source, const QString &destination); - bool moveDirectory(const QString &source, const QString &destination); - bool extractCab(const QString &cabFile, const QString &destination); bool extractFile(Unshield *unshield, const QString &destination, const QString &prefix, int index, int counter); bool findInCab(const QString &cabFile, const QString &fileName); - QString findFile(const QString &fileName, const QString &path, int depth = 0); - QStringList findFiles(const QString &fileName, const QString &path, int depth = 0); + QString findFile(const QString &fileName, const QString &path); - bool installFile(const QString &fileName, const QString &path); - bool installDirectory(const QString &dirName, const QString &path, bool recursive = true); + QStringList findFiles(const QString &fileName, const QString &path, int depth = 0, bool recursive = true, + bool directories = false, Qt::MatchFlags flags = Qt::MatchExactly); + + QStringList findDirectories(const QString &dirName, const QString &path, bool recursive = true); + + bool installFile(const QString &fileName, const QString &path, Qt::MatchFlags flags = Qt::MatchExactly, + bool keepSource = false); + + bool installFiles(const QString &fileName, const QString &path, Qt::MatchFlags flags = Qt::MatchExactly, + bool keepSource = false, bool single = false); + + bool installDirectories(const QString &dirName, const QString &path, + bool recursive = true, bool keepSource = false); bool installComponent(Component component, const QString &path); bool setupComponent(Component component); @@ -111,7 +117,6 @@ namespace Wizard void error(const QString &text, const QString &details); void progressChanged(int progress); - }; } From ae5f783a1650b8d65a2b65eeae149ab5729ef473 Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 17 Mar 2014 17:50:51 +0100 Subject: [PATCH 040/303] Added some eye-candy to the wizard: a watermark and a placeholder icon --- apps/wizard/intropage.cpp | 2 +- apps/wizard/mainwizard.cpp | 3 ++- files/ui/wizard/intropage.ui | 6 ++++-- files/wizard/images/intropage-background.png | Bin 0 -> 150614 bytes files/wizard/images/openmw-wizard.png | Bin 0 -> 72530 bytes files/wizard/wizard.qrc | 4 ++++ 6 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 files/wizard/images/intropage-background.png create mode 100644 files/wizard/images/openmw-wizard.png diff --git a/apps/wizard/intropage.cpp b/apps/wizard/intropage.cpp index 93510cd210..91e7d5dc07 100644 --- a/apps/wizard/intropage.cpp +++ b/apps/wizard/intropage.cpp @@ -5,9 +5,9 @@ Wizard::IntroPage::IntroPage(MainWizard *wizard) : QWizardPage(wizard), mWizard(wizard) - { setupUi(this); + setPixmap(QWizard::WatermarkPixmap, QPixmap(QLatin1String(":/images/intropage-background.png"))); } int Wizard::IntroPage::nextId() const diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 2fec3cec38..6a72d71fbc 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -25,7 +25,6 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : mGameSettings(mCfgMgr), QWizard(parent) { - #ifndef Q_OS_MAC setWizardStyle(QWizard::ModernStyle); #else @@ -33,6 +32,8 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : #endif setWindowTitle(tr("OpenMW Wizard")); + setWindowIcon(QIcon(QLatin1String(":/images/openmw-wizard.png"))); + setMinimumWidth(550); // Set the property for comboboxes to the text instead of index setDefaultProperty("QComboBox", "currentText", "currentIndexChanged"); diff --git a/files/ui/wizard/intropage.ui b/files/ui/wizard/intropage.ui index bb597e3a61..b06f7f170c 100644 --- a/files/ui/wizard/intropage.ui +++ b/files/ui/wizard/intropage.ui @@ -20,7 +20,7 @@ - This Wizard will help you install Morrowind and its add-ons for OpenMW to use. + This Wizard will help you install Morrowindand its add-ons for OpenMW to use. true @@ -29,6 +29,8 @@ - + + + diff --git a/files/wizard/images/intropage-background.png b/files/wizard/images/intropage-background.png new file mode 100644 index 0000000000000000000000000000000000000000..0ce13804b4f5b3a2ad8338c8f6a5a88870acf778 GIT binary patch literal 150614 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�zTe*@)AGZt5mTa*$uAtDLDJ!rOVe5> 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 literal 0 HcmV?d00001 diff --git a/files/wizard/wizard.qrc b/files/wizard/wizard.qrc index b3bb722c9f..99623e9856 100644 --- a/files/wizard/wizard.qrc +++ b/files/wizard/wizard.qrc @@ -5,4 +5,8 @@ icons/tango/48x48/folder.png icons/tango/48x48/system-installer.png + + images/intropage-background.png + images/openmw-wizard.png + From 9e5e19f9cb6387f0d9f50bfe820ddc0e2ffcebd4 Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 17 Mar 2014 18:28:05 +0100 Subject: [PATCH 041/303] Improved existing installation handling --- apps/wizard/existinginstallationpage.cpp | 95 ++++++++++++--------- apps/wizard/existinginstallationpage.hpp | 3 + apps/wizard/mainwizard.hpp | 1 - files/ui/wizard/existinginstallationpage.ui | 11 +-- 4 files changed, 60 insertions(+), 50 deletions(-) diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 91d4f2df1c..56743eb44a 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -14,6 +14,12 @@ Wizard::ExistingInstallationPage::ExistingInstallationPage(MainWizard *wizard) : { setupUi(this); + mEmptyItem = new QListWidgetItem(tr("No existing installations detected")); + mEmptyItem->setFlags(Qt::NoItemFlags); + installationsList->addItem(mEmptyItem); + mEmptyItem->setHidden(true); + + connect(installationsList, SIGNAL(currentTextChanged(QString)), this, SLOT(textChanged(QString))); @@ -21,51 +27,17 @@ Wizard::ExistingInstallationPage::ExistingInstallationPage(MainWizard *wizard) : this, SIGNAL(completeChanged())); } -void Wizard::ExistingInstallationPage::on_browseButton_clicked() -{ - QString selectedFile = QFileDialog::getOpenFileName( - this, - tr("Select master file"), - QDir::currentPath(), - QString(tr("Morrowind master file (*.esm)")), - NULL, - QFileDialog::DontResolveSymlinks); - - QFileInfo info(selectedFile); - if (!info.exists()) - return; - - QString path(QDir::toNativeSeparators(info.absolutePath())); - QList items = installationsList->findItems(path, Qt::MatchExactly); - - if (items.isEmpty()) { - // Path is not yet in the list, add it - mWizard->addInstallation(path); - - QListWidgetItem *item = new QListWidgetItem(path); - installationsList->addItem(item); - installationsList->setCurrentItem(item); // Select it too - } else { - installationsList->setCurrentItem(items.first()); - } - -} - -void Wizard::ExistingInstallationPage::textChanged(const QString &text) -{ - // Set the installation path manually, as registerField doesn't work - // Because it doesn't accept two widgets operating on a single field - if (!text.isEmpty()) - mWizard->setField(QLatin1String("installation.path"), text); -} - void Wizard::ExistingInstallationPage::initializePage() { QStringList paths(mWizard->mInstallations.keys()); - if (paths.isEmpty()) + if (paths.isEmpty()) { + mEmptyItem->setHidden(false); return; + } + // Make to clear list before adding items + // to prevent duplicates when going back and forth between pages installationsList->clear(); foreach (const QString &path, paths) { @@ -119,6 +91,51 @@ bool Wizard::ExistingInstallationPage::validatePage() return true; } +void Wizard::ExistingInstallationPage::on_browseButton_clicked() +{ + QString selectedFile = QFileDialog::getOpenFileName( + this, + tr("Select master file"), + QDir::currentPath(), + QString(tr("Morrowind master file (*.esm)")), + NULL, + QFileDialog::DontResolveSymlinks); + + QFileInfo info(selectedFile); + + if (!info.exists()) + return; + + if (!mWizard->findFiles(QLatin1String("Morrowind"), info.absolutePath())) + return; // No valid Morrowind installation found + + QString path(QDir::toNativeSeparators(info.absolutePath())); + QList items = installationsList->findItems(path, Qt::MatchExactly); + + if (items.isEmpty()) { + // Path is not yet in the list, add it + mWizard->addInstallation(path); + + // Hide the default item + mEmptyItem->setHidden(true); + + QListWidgetItem *item = new QListWidgetItem(path); + installationsList->addItem(item); + installationsList->setCurrentItem(item); // Select it too + } else { + installationsList->setCurrentItem(items.first()); + } + +} + +void Wizard::ExistingInstallationPage::textChanged(const QString &text) +{ + // Set the installation path manually, as registerField doesn't work + // Because it doesn't accept two widgets operating on a single field + if (!text.isEmpty()) + mWizard->setField(QLatin1String("installation.path"), text); +} + bool Wizard::ExistingInstallationPage::isComplete() const { if (installationsList->selectionModel()->hasSelection()) { diff --git a/apps/wizard/existinginstallationpage.hpp b/apps/wizard/existinginstallationpage.hpp index d243bb8689..04385893ac 100644 --- a/apps/wizard/existinginstallationpage.hpp +++ b/apps/wizard/existinginstallationpage.hpp @@ -2,6 +2,7 @@ #define EXISTINGINSTALLATIONPAGE_HPP #include +#include #include "ui_existinginstallationpage.h" @@ -26,6 +27,8 @@ namespace Wizard private: MainWizard *mWizard; + QListWidgetItem *mEmptyItem; + protected: void initializePage(); diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index 962571ac83..f55709badc 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -9,7 +9,6 @@ namespace Wizard { - class MainWizard : public QWizard { Q_OBJECT diff --git a/files/ui/wizard/existinginstallationpage.ui b/files/ui/wizard/existinginstallationpage.ui index b887fb06cc..921b2eb2a3 100644 --- a/files/ui/wizard/existinginstallationpage.ui +++ b/files/ui/wizard/existinginstallationpage.ui @@ -28,16 +28,7 @@ - - - - No existing installations detected - - - NoItemFlags - - - + From 2e1248537e73e28e5ee8c345507cf8c59cca893b Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 17 Mar 2014 21:33:44 +0100 Subject: [PATCH 042/303] Minor fixes, installation should stop after one error message now --- apps/wizard/conclusionpage.cpp | 10 +++++----- apps/wizard/unshield/unshieldworker.cpp | 17 +++++++++-------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index 7b638813b7..932a9715c3 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -13,11 +13,11 @@ Wizard::ConclusionPage::ConclusionPage(MainWizard *wizard) : void Wizard::ConclusionPage::initializePage() { -// // Write the path to openmw.cfg -// if (field(QLatin1String("installation.new")).toBool() == true) { -// QString path(field(QLatin1String("installation.path")).toString()); -// mWizard->addInstallation(path); -// } + // Write the path to openmw.cfg + if (field(QLatin1String("installation.new")).toBool() == true) { + QString path(field(QLatin1String("installation.path")).toString()); + mWizard->addInstallation(path); + } if (!mWizard->mError) { diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 2319ca0df5..aceac69b88 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -251,16 +251,15 @@ bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString { QDir sourceDir(source); QDir destDir(destination); - bool result = false; + bool result = true; if (!destDir.exists()) { - if (!sourceDir.mkpath(destination)) { + if (!sourceDir.mkpath(destination)) return false; - } - - destDir.refresh(); } + destDir.refresh(); + if (!destDir.exists()) return false; @@ -387,7 +386,7 @@ void Wizard::UnshieldWorker::extract() writeSettings(); // Remove the temporary directory - removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); + //removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); // Fill the progress bar int total = 0; @@ -572,6 +571,7 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString if (!installDirectories(dir, info.absolutePath(), false, true)) { emit error(tr("Could not install directory!"), tr("Installing %1 to %2 failed.").arg(dir, info.absolutePath())); + return false; } } @@ -626,6 +626,7 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString if (!copyDirectory(sounds.absoluteFilePath(), dest)) { emit error(tr("Could not install directory!"), tr("Installing %1 to %2 failed.").arg(sounds.absoluteFilePath(), dest)); + return false; } } @@ -699,7 +700,7 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &destination, const QString &prefix, int index, int counter) { - bool success; + bool success = false; QString path(destination); path.append(QDir::separator()); @@ -845,7 +846,7 @@ bool Wizard::UnshieldWorker::findInCab(const QString &cabFile, const QString &fi bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &destination) { - bool success; + bool success = false; QByteArray array(cabFile.toUtf8()); From 667b0de250149fcb19d03ba065a1dc077dde828e Mon Sep 17 00:00:00 2001 From: pvdk Date: Mon, 17 Mar 2014 23:39:21 +0100 Subject: [PATCH 043/303] Fixed problems with cd content autodetection --- apps/wizard/unshield/unshieldworker.cpp | 111 ++++++++++++++---------- apps/wizard/unshield/unshieldworker.hpp | 4 +- 2 files changed, 70 insertions(+), 45 deletions(-) diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index aceac69b88..679730715f 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -386,7 +386,7 @@ void Wizard::UnshieldWorker::extract() writeSettings(); // Remove the temporary directory - //removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); + removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); // Fill the progress bar int total = 0; @@ -445,26 +445,39 @@ bool Wizard::UnshieldWorker::setupComponent(Component component) foreach (const QString &file, list) { + qDebug() << "current archive: " << file; + + // Try to open the archive + Unshield *unshield = NULL; + unshield = openCab(file); + + if (!unshield) + return false; + if (component == Wizard::Component_Morrowind) { - bool morrowindFound = findInCab(file, QLatin1String("Morrowind.bsa")); - bool tribunalFound = findInCab(file, QLatin1String("Tribunal.bsa")); - bool bloodmoonFound = findInCab(file, QLatin1String("Bloodmoon.bsa")); + bool morrowindFound = findInCab(QLatin1String("Morrowind.bsa"), unshield); + bool tribunalFound = findInCab(QLatin1String("Tribunal.bsa"), unshield); + bool bloodmoonFound = findInCab(QLatin1String("Bloodmoon.bsa"), unshield); if (morrowindFound) { // Check if we have correct archive, other archives have Morrowind.bsa too if ((tribunalFound && bloodmoonFound) || (!tribunalFound && !bloodmoonFound)) { cabFile = file; - found = true; // We have a GoTY disk + found = true; // We have a GoTY disk or a Morrowind-only disk } } } else { - if (findInCab(file, name + QLatin1String(".bsa"))) { + + if (findInCab(name + QLatin1String(".bsa"), unshield)) { cabFile = file; found = true; } } + + // Close the current archive + unshield_close(unshield); } if (!found) { @@ -746,6 +759,39 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &dest return success; } +bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &destination) +{ + bool success = false; + + QByteArray array(cabFile.toUtf8()); + + Unshield *unshield; + unshield = unshield_open(array.constData()); + + if (!unshield) { + emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); + return false; + } + + int counter = 0; + + for (int i=0; ifirst_file; j<=group->last_file; ++j) + { + if (unshield_file_is_valid(unshield, j)) { + success = extractFile(unshield, destination, group->name, j, counter); + ++counter; + } + } + } + + unshield_close(unshield); + return success; +} + QString Wizard::UnshieldWorker::findFile(const QString &fileName, const QString &path) { return findFiles(fileName, path).first(); @@ -764,14 +810,16 @@ QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QSt QStringList result; QDir dir(path); + // Prevent parsing over the complete filesystem + if (dir == QDir::rootPath()) + return QStringList(); + if (!dir.exists()) return QStringList(); QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | - QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst)); foreach(QFileInfo info, list) { - if (info.isSymLink()) continue; @@ -813,7 +861,7 @@ QStringList Wizard::UnshieldWorker::findDirectories(const QString &dirName, cons return findFiles(dirName, path, 0, true, true); } -bool Wizard::UnshieldWorker::findInCab(const QString &cabFile, const QString &fileName) +Unshield* Wizard::UnshieldWorker::openCab(const QString &cabFile) { QByteArray array(cabFile.toUtf8()); @@ -822,9 +870,18 @@ bool Wizard::UnshieldWorker::findInCab(const QString &cabFile, const QString &fi if (!unshield) { emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); - return false; + unshield_close(unshield); + return NULL; } + return unshield; +} + +bool Wizard::UnshieldWorker::findInCab(const QString &fileName, Unshield *unshield) +{ + if (!unshield) + return false; + for (int i=0; ifirst_file; j<=group->last_file; ++j) - { - if (unshield_file_is_valid(unshield, j)) { - success = extractFile(unshield, destination, group->name, j, counter); - ++counter; - } - } - } - - unshield_close(unshield); - return success; -} diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index f23dc1aa76..36215bf5d7 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -62,7 +62,9 @@ namespace Wizard bool extractCab(const QString &cabFile, const QString &destination); bool extractFile(Unshield *unshield, const QString &destination, const QString &prefix, int index, int counter); - bool findInCab(const QString &cabFile, const QString &fileName); + + Unshield* openCab(const QString &cabFile); + bool findInCab(const QString &fileName, Unshield *unshield); QString findFile(const QString &fileName, const QString &path); From ef617fdf3df484563ef3fd3e1a515042766b57e9 Mon Sep 17 00:00:00 2001 From: pvdk Date: Tue, 18 Mar 2014 00:33:31 +0100 Subject: [PATCH 044/303] Fail on the first error whilst extracting an archive --- apps/wizard/unshield/unshieldworker.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 679730715f..70c780b3b8 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -752,7 +752,7 @@ bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &dest success = unshield_file_save(unshield, index, array.constData()); if (!success) { - emit error(tr("Failed to extract %1.").arg(QString::fromUtf8(unshield_file_name(unshield, index))), tr("Complete path: %1.").arg(fileName)); + qDebug() << "error"; dir.remove(fileName); } @@ -783,6 +783,15 @@ bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &d { if (unshield_file_is_valid(unshield, j)) { success = extractFile(unshield, destination, group->name, j, counter); + + if (!success) { + QString name(QString::fromUtf8(unshield_file_name(unshield, j))); + + emit error(tr("Failed to extract %1.").arg(name), + tr("Complete path: %1").arg(destination + QDir::separator() + name)); + return false; + } + ++counter; } } From eb04fa85b7076d83e59fe28f1a9b97bb0109d0b7 Mon Sep 17 00:00:00 2001 From: pvdk Date: Sat, 29 Mar 2014 11:57:25 +0100 Subject: [PATCH 045/303] Added background image to the last page too --- apps/wizard/conclusionpage.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index 932a9715c3..4407ac5475 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -9,6 +9,7 @@ Wizard::ConclusionPage::ConclusionPage(MainWizard *wizard) : mWizard(wizard) { setupUi(this); + setPixmap(QWizard::WatermarkPixmap, QPixmap(QLatin1String(":/images/intropage-background.png"))); } void Wizard::ConclusionPage::initializePage() From 10d2ca82f7a90a28b944057a32805a28c010b13e Mon Sep 17 00:00:00 2001 From: pvdk Date: Sun, 30 Mar 2014 22:58:50 +0200 Subject: [PATCH 046/303] Implemented a simple logger --- apps/wizard/existinginstallationpage.cpp | 1 - apps/wizard/installationpage.cpp | 11 +++++- apps/wizard/mainwizard.cpp | 44 ++++++++++++++++++++++++ apps/wizard/mainwizard.hpp | 6 ++++ apps/wizard/unshield/unshieldworker.cpp | 24 ++++++++----- apps/wizard/unshield/unshieldworker.hpp | 5 ++- 6 files changed, 78 insertions(+), 13 deletions(-) diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 56743eb44a..d37dd949d6 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -19,7 +19,6 @@ Wizard::ExistingInstallationPage::ExistingInstallationPage(MainWizard *wizard) : installationsList->addItem(mEmptyItem); mEmptyItem->setHidden(true); - connect(installationsList, SIGNAL(currentTextChanged(QString)), this, SLOT(textChanged(QString))); diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 74666c394c..a0d3dfd21b 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -80,6 +80,9 @@ void Wizard::InstallationPage::startInstallation() connect(mUnshield, SIGNAL(textChanged(QString)), logTextEdit, SLOT(appendPlainText(QString)), Qt::QueuedConnection); + connect(mUnshield, SIGNAL(textChanged(QString)), + mWizard, SLOT(addLogText(QString)), Qt::QueuedConnection); + connect(mUnshield, SIGNAL(progressChanged(int)), installProgressBar, SLOT(setValue(int)), Qt::QueuedConnection); @@ -153,6 +156,8 @@ void Wizard::InstallationPage::showFileDialog(Wizard::Component component) if (path.isEmpty()) { logTextEdit->appendHtml(tr("


\ Error: The installation was aborted by the user

")); + + mWizard->addLogText(QLatin1String("Error: The installation was aborted by the user")); mWizard->mError = true; emit completeChanged(); @@ -185,6 +190,10 @@ void Wizard::InstallationPage::installationError(const QString &text, const QStr logTextEdit->appendHtml(tr("

\ %1

").arg(details)); + mWizard->addLogText(QLatin1String("Error: ") + text); + mWizard->addLogText(details); + + mWizard->mError = true; QMessageBox msgBox; msgBox.setWindowTitle(tr("An error occurred")); msgBox.setIcon(QMessageBox::Critical); @@ -196,7 +205,7 @@ void Wizard::InstallationPage::installationError(const QString &text, const QStr msgBox.setDetailedText(details); msgBox.exec(); - mWizard->mError = true; + emit completeChanged(); } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 6a72d71fbc..11e44c6b06 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -4,6 +4,8 @@ #include +#include +#include #include #include #include @@ -40,11 +42,53 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setDefaultProperty("ComponentListWidget", "mCheckedItems", "checkedItemsChanged"); + setupLog(); setupGameSettings(); setupInstallations(); setupPages(); } +void Wizard::MainWizard::setupLog() +{ + QString logPath(QString::fromUtf8(mCfgMgr.getLogPath().string().c_str())); + logPath.append(QLatin1String("wizard.log")); + + QString message(tr("

Could not open %1 for writing

\ +

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

")); + + QFile *file = new QFile(logPath); + + if (!file->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening Wizard log file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(message.arg(file->fileName())); + msgBox.exec(); + return qApp->quit(); + } + + qDebug() << logPath; + + mLog = new QTextStream(file); + mLog->setCodec(QTextCodec::codecForName("UTF-8")); + + //addLogText(QLatin1String("test test 123 test")); +} + +void Wizard::MainWizard::addLogText(const QString &text) +{ + + qDebug() << "AddLogText called! " << text; + if (!text.isEmpty()) { + qDebug() << "logging " << text; + *mLog << text << endl; + } + +// file.close(); +} + void Wizard::MainWizard::setupGameSettings() { QString userPath(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index f55709badc..dcb07a8c2b 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -46,8 +46,12 @@ namespace Wizard bool mError; + public slots: + void addLogText(const QString &text); + private: + void setupLog(); void setupGameSettings(); void setupInstallations(); void setupPages(); @@ -56,6 +60,8 @@ namespace Wizard Config::GameSettings mGameSettings; + QTextStream *mLog; + private slots: void accept(); void reject(); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 70c780b3b8..f0b9a86745 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -153,37 +153,39 @@ void Wizard::UnshieldWorker::setIniCodec(QTextCodec *codec) mIniCodec = codec; } -void Wizard::UnshieldWorker::setupSettings() +bool Wizard::UnshieldWorker::setupSettings() { // Create Morrowind.ini settings map if (getIniPath().isEmpty()) - return; + return false; QFile file(getIniPath()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { emit error(tr("Failed to open Morrowind configuration file!"), tr("Opening %1 failed: %2.").arg(getIniPath(), file.errorString())); - return; + return false; } QTextStream stream(&file); stream.setCodec(mIniCodec); mIniSettings.readFile(stream); + + return true; } -void Wizard::UnshieldWorker::writeSettings() +bool Wizard::UnshieldWorker::writeSettings() { if (getIniPath().isEmpty()) - return; + return false; QFile file(getIniPath()); if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) { emit error(tr("Failed to open Morrowind configuration file!"), tr("Opening %1 failed: %2.").arg(getIniPath(), file.errorString())); - return; + return false; } QTextStream stream(&file); @@ -192,7 +194,10 @@ void Wizard::UnshieldWorker::writeSettings() if (!mIniSettings.writeFile(getIniPath(), stream)) { emit error(tr("Failed to write Morrowind configuration file!"), tr("Writing to %1 failed: %2.").arg(getIniPath(), file.errorString())); + return false; } + + return true; } bool Wizard::UnshieldWorker::removeDirectory(const QString &dirName) @@ -383,7 +388,8 @@ void Wizard::UnshieldWorker::extract() } // Write the settings to the Morrowind config file - writeSettings(); + if (!writeSettings()) + return false; // Remove the temporary directory removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); @@ -626,7 +632,9 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString // Setup Morrowind configuration setIniPath(getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); - setupSettings(); + + if (!setupSettings()) + return false; } if (component == Wizard::Component_Tribunal) diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index 36215bf5d7..e5a712977e 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -42,11 +42,11 @@ namespace Wizard void setIniCodec(QTextCodec *codec); - void setupSettings(); + bool setupSettings(); private: - void writeSettings(); + bool writeSettings(); bool getInstallComponent(Component component); @@ -114,7 +114,6 @@ namespace Wizard void requestFileDialog(Wizard::Component component); void textChanged(const QString &text); - void logTextChanged(const QString &text); void error(const QString &text, const QString &details); void progressChanged(int progress); From 14b164a03d60615f67a0e0cb97a7fe93c36f2fcd Mon Sep 17 00:00:00 2001 From: pvdk Date: Sun, 30 Mar 2014 23:11:38 +0200 Subject: [PATCH 047/303] Reverted an unneeded fix --- apps/wizard/unshield/unshieldworker.cpp | 32 +++++++------------------ apps/wizard/unshield/unshieldworker.hpp | 3 +-- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index f0b9a86745..81282f5477 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -389,7 +389,7 @@ void Wizard::UnshieldWorker::extract() // Write the settings to the Morrowind config file if (!writeSettings()) - return false; + return; // Remove the temporary directory removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); @@ -453,18 +453,11 @@ bool Wizard::UnshieldWorker::setupComponent(Component component) qDebug() << "current archive: " << file; - // Try to open the archive - Unshield *unshield = NULL; - unshield = openCab(file); - - if (!unshield) - return false; - if (component == Wizard::Component_Morrowind) { - bool morrowindFound = findInCab(QLatin1String("Morrowind.bsa"), unshield); - bool tribunalFound = findInCab(QLatin1String("Tribunal.bsa"), unshield); - bool bloodmoonFound = findInCab(QLatin1String("Bloodmoon.bsa"), unshield); + bool morrowindFound = findInCab(QLatin1String("Morrowind.bsa"), file); + bool tribunalFound = findInCab(QLatin1String("Tribunal.bsa"), file); + bool bloodmoonFound = findInCab(QLatin1String("Bloodmoon.bsa"), file); if (morrowindFound) { // Check if we have correct archive, other archives have Morrowind.bsa too @@ -476,14 +469,12 @@ bool Wizard::UnshieldWorker::setupComponent(Component component) } } else { - if (findInCab(name + QLatin1String(".bsa"), unshield)) { + if (findInCab(name + QLatin1String(".bsa"), file)) { cabFile = file; found = true; } } - // Close the current archive - unshield_close(unshield); } if (!found) { @@ -778,6 +769,7 @@ bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &d if (!unshield) { emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); + unshield_close(unshield); return false; } @@ -878,7 +870,7 @@ QStringList Wizard::UnshieldWorker::findDirectories(const QString &dirName, cons return findFiles(dirName, path, 0, true, true); } -Unshield* Wizard::UnshieldWorker::openCab(const QString &cabFile) +bool Wizard::UnshieldWorker::findInCab(const QString &fileName, const QString &cabFile) { QByteArray array(cabFile.toUtf8()); @@ -888,16 +880,8 @@ Unshield* Wizard::UnshieldWorker::openCab(const QString &cabFile) if (!unshield) { emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); unshield_close(unshield); - return NULL; - } - - return unshield; -} - -bool Wizard::UnshieldWorker::findInCab(const QString &fileName, Unshield *unshield) -{ - if (!unshield) return false; + } for (int i=0; i Date: Sun, 30 Mar 2014 23:57:30 +0200 Subject: [PATCH 048/303] Started working on the settings tab --- apps/launcher/settingspage.cpp | 17 +++++++++++++++++ apps/launcher/settingspage.hpp | 7 +++++++ files/ui/settingspage.ui | 11 +++++++---- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 96dd6e5299..142af5f790 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -1,5 +1,11 @@ #include "settingspage.hpp" +#include + +#include + +using namespace Process; + Launcher::SettingsPage::SettingsPage(QWidget *parent) : QWidget(parent) { setupUi(this); @@ -16,3 +22,14 @@ Launcher::SettingsPage::SettingsPage(QWidget *parent) : QWidget(parent) languageComboBox->addItems(languages); } +void Launcher::SettingsPage::on_wizardButton_clicked() +{ + if (!ProcessInvoker::startProcess(QLatin1String("openmw-wizard"), true)) + qDebug() << "an error occurred"; + +} + +void Launcher::SettingsPage::on_importerButton_clicked() +{ + +} diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index c87f6e46ff..4b07732442 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -11,9 +11,16 @@ namespace Launcher class SettingsPage : public QWidget, private Ui::SettingsPage { Q_OBJECT + public: SettingsPage(QWidget *parent = 0); + void saveSettings(); + bool loadSettings(); + + private slots: + void on_wizardButton_clicked(); + void on_importerButton_clicked(); }; } diff --git a/files/ui/settingspage.ui b/files/ui/settingspage.ui index 95b62761e6..ce5aa57ece 100644 --- a/files/ui/settingspage.ui +++ b/files/ui/settingspage.ui @@ -6,8 +6,8 @@ 0 0 - 518 - 401 + 516 + 399
@@ -22,8 +22,11 @@ + + <html><head/><body><p>The language of the original Morrowind installation files (used for the character encoding)</p></body></html> + - Morrowind installation language: + Morrowind content language: @@ -105,7 +108,7 @@ - Import previously selected add-ons (creates a new Content List) + Import add-on and plugin selection (creates a new Content List) true From 21c406316f21cde0922efdde7f9e7a54184d2fa5 Mon Sep 17 00:00:00 2001 From: pvdk Date: Wed, 16 Apr 2014 16:54:55 +0200 Subject: [PATCH 049/303] Working on the Settings tab: start the importer/wizard --- apps/launcher/datafilespage.cpp | 4 +- apps/launcher/maindialog.cpp | 32 +++++- apps/launcher/maindialog.hpp | 4 +- apps/launcher/settingspage.cpp | 118 ++++++++++++++++++-- apps/launcher/settingspage.hpp | 32 +++++- apps/launcher/utils/textinputdialog.cpp | 46 +++----- apps/launcher/utils/textinputdialog.hpp | 20 ++-- apps/wizard/mainwizard.cpp | 4 +- components/config/launchersettings.cpp | 10 +- components/process/processinvoker.cpp | 139 ++++++++++++++++++------ components/process/processinvoker.hpp | 30 ++++- files/ui/settingspage.ui | 12 +- 12 files changed, 342 insertions(+), 109 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index db59751027..efd9765aca 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -1,5 +1,7 @@ #include "datafilespage.hpp" +#include + #include #include #include @@ -221,7 +223,7 @@ void Launcher::DataFilesPage::on_newProfileAction_triggered() if (newDialog.exec() != QDialog::Accepted) return; - QString profile = newDialog.getText(); + QString profile = newDialog.lineEdit()->text(); if (profile.isEmpty()) return; diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 7f129f4e97..b8f9efd1af 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include @@ -138,7 +137,7 @@ void Launcher::MainDialog::createPages() mPlayPage = new PlayPage(this); mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this); mGraphicsPage = new GraphicsPage(mCfgMgr, mGraphicsSettings, this); - mSettingsPage = new SettingsPage(this); + mSettingsPage = new SettingsPage(mGameSettings, mLauncherSettings, this); // Set the combobox of the play page to imitate the combobox on the datafilespage mPlayPage->setProfilesModel(mDataFilesPage->profilesModel()); @@ -285,7 +284,9 @@ bool Launcher::MainDialog::showFirstRunDialog() arguments.append(QString("--cfg")); arguments.append(path); - if (!ProcessInvoker::startProcess(QLatin1String("mwiniimport"), arguments, false)) + ProcessInvoker invoker(this); + + if (!invoker.startProcess(QLatin1String("mwiniimport"), arguments, false)) return false; // Re-read the game settings @@ -337,6 +338,26 @@ bool Launcher::MainDialog::setup() return true; } +bool Launcher::MainDialog::reloadSettings() +{ + if (!setupLauncherSettings()) + return false; + + if (!setupGameSettings()) + return false; + + if (!setupGraphicsSettings()) + return false; + +// if (!mSettingsPage->loadSettings()) +// return false; + + if (!mGraphicsPage->loadSettings()) + return false; + + return true; +} + void Launcher::MainDialog::changePage(QListWidgetItem *current, QListWidgetItem *previous) { if (!current) @@ -799,6 +820,7 @@ bool Launcher::MainDialog::writeSettings() void Launcher::MainDialog::closeEvent(QCloseEvent *event) { + qDebug() << "close event!"; writeSettings(); event->accept(); } @@ -822,6 +844,8 @@ void Launcher::MainDialog::play() } // Launch the game detached - if (ProcessInvoker::startProcess(QLatin1String("openmw"), true)) + ProcessInvoker invoker(this); + + if (invoker.startProcess(QLatin1String("openmw"), true)) qApp->quit(); } diff --git a/apps/launcher/maindialog.hpp b/apps/launcher/maindialog.hpp index 718acb5a9b..7f468de9c8 100644 --- a/apps/launcher/maindialog.hpp +++ b/apps/launcher/maindialog.hpp @@ -39,6 +39,9 @@ namespace Launcher bool setup(); bool showFirstRunDialog(); + bool reloadSettings(); + bool writeSettings(); + public slots: void changePage(QListWidgetItem *current, QListWidgetItem *previous); void play(); @@ -53,7 +56,6 @@ namespace Launcher void loadSettings(); void saveSettings(); - bool writeSettings(); 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); diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 142af5f790..b2d03ba952 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -1,12 +1,21 @@ #include "settingspage.hpp" -#include - +#include #include +#include +#include + +#include "utils/textinputdialog.hpp" + using namespace Process; -Launcher::SettingsPage::SettingsPage(QWidget *parent) : QWidget(parent) +Launcher::SettingsPage::SettingsPage(Config::GameSettings &gameSettings, + Config::LauncherSettings &launcherSettings, MainDialog *parent) : + mGameSettings(gameSettings), + mLauncherSettings(launcherSettings), + QWidget(parent), + mMain(parent) { setupUi(this); @@ -20,16 +29,111 @@ Launcher::SettingsPage::SettingsPage(QWidget *parent) : QWidget(parent) << "Spanish"; languageComboBox->addItems(languages); + + mWizardInvoker = new ProcessInvoker(this); + mImporterInvoker = new ProcessInvoker(this); + + 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 Profile"), tr("Profile name:"), this); + + connect(mProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), + this, SLOT(updateOkButton(QString))); + +// // Detect Morrowind configuration files +// foreach (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"))); +// } +// } } void Launcher::SettingsPage::on_wizardButton_clicked() { - if (!ProcessInvoker::startProcess(QLatin1String("openmw-wizard"), true)) - qDebug() << "an error occurred"; - + if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) + return; } void Launcher::SettingsPage::on_importerButton_clicked() { - + if (!mImporterInvoker->startProcess(QLatin1String("mwiniimport"), false)) + return; +} + +void Launcher::SettingsPage::wizardStarted() +{ + qDebug() << "wizard started!"; + wizardButton->setEnabled(false); +} + +void Launcher::SettingsPage::wizardFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + qDebug() << "wizard finished!"; + if (exitCode != 0 || exitStatus == QProcess::CrashExit) + return; + + mMain->writeSettings(); + mMain->reloadSettings(); + wizardButton->setEnabled(true); +} + +void Launcher::SettingsPage::importerStarted() +{ + qDebug() << "importer started!"; + importerButton->setEnabled(false); +} + +void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + qDebug() << "importer finished!"; + if (exitCode != 0 || exitStatus == QProcess::CrashExit) + return; + + mMain->writeSettings(); + mMain->reloadSettings(); + + + if (addonsCheckBox->isChecked()) { + + if (mProfileDialog->exec() == QDialog::Accepted) { + QString profile = mProfileDialog->lineEdit()->text(); + qDebug() << profile; + } + } + + importerButton->setEnabled(true); +} + +void Launcher::SettingsPage::updateOkButton(const QString &text) +{ + // We do this here because we need the profiles combobox text + if (text.isEmpty()) { + mProfileDialog->setOkButtonEnabled(false); + return; + } + + +// (profilesComboBox->findText(text) == -1) +// ? mNewProfileDialog->setOkButtonEnabled(true) +// : mNewProfileDialog->setOkButtonEnabled(false); } diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index 4b07732442..ed5c068fb1 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -2,18 +2,28 @@ #define SETTINGSPAGE_HPP #include +#include + +#include #include "ui_settingspage.h" +#include "maindialog.hpp" + +namespace Config { class GameSettings; + class LauncherSettings; } + namespace Launcher { + class TextInputDialog; class SettingsPage : public QWidget, private Ui::SettingsPage { Q_OBJECT public: - SettingsPage(QWidget *parent = 0); + SettingsPage( Config::GameSettings &gameSettings, + Config::LauncherSettings &launcherSettings, MainDialog *parent = 0); void saveSettings(); bool loadSettings(); @@ -21,6 +31,26 @@ namespace Launcher private slots: void on_wizardButton_clicked(); void on_importerButton_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; + + Config::GameSettings &mGameSettings; + Config::LauncherSettings &mLauncherSettings; + + MainDialog *mMain; + TextInputDialog *mProfileDialog; + + }; } diff --git a/apps/launcher/utils/textinputdialog.cpp b/apps/launcher/utils/textinputdialog.cpp index 76cbe32d01..74e40a81c2 100644 --- a/apps/launcher/utils/textinputdialog.cpp +++ b/apps/launcher/utils/textinputdialog.cpp @@ -14,17 +14,17 @@ Launcher::TextInputDialog::TextInputDialog(const QString& title, const QString & mButtonBox = new QDialogButtonBox(this); mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); - mButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); +// 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 DialogLineEdit(this); + mLineEdit = new LineEdit(this); mLineEdit->setValidator(validator); mLineEdit->setCompleter(0); - QLabel *label = new QLabel(this); - label->setText(text); - QVBoxLayout *dialogLayout = new QVBoxLayout(this); dialogLayout->addWidget(label); dialogLayout->addWidget(mLineEdit); @@ -41,8 +41,10 @@ Launcher::TextInputDialog::TextInputDialog(const QString& title, const QString & connect(mButtonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(mButtonBox, SIGNAL(rejected()), this, SLOT(reject())); - connect(mLineEdit, SIGNAL(textChanged(QString)), this, SLOT(slotUpdateOkButton(QString))); +} +Launcher::TextInputDialog::~TextInputDialog() +{ } int Launcher::TextInputDialog::exec() @@ -52,36 +54,18 @@ int Launcher::TextInputDialog::exec() return QDialog::exec(); } -QString Launcher::TextInputDialog::getText() const +void Launcher::TextInputDialog::setOkButtonEnabled(bool enabled) { - return mLineEdit->text(); -} + QPushButton *okButton = mButtonBox->button(QDialogButtonBox::Ok); + okButton->setEnabled(enabled); -void Launcher::TextInputDialog::slotUpdateOkButton(QString text) -{ - bool enabled = !(text.isEmpty()); - mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(enabled); + QPalette *palette = new QPalette(); + palette->setColor(QPalette::Text, Qt::red); - if (enabled) + if (enabled) { mLineEdit->setPalette(QApplication::palette()); - else - { + } else { // Existing profile name, make the text red - QPalette *palette = new QPalette(); - palette->setColor(QPalette::Text,Qt::red); mLineEdit->setPalette(*palette); } } - -Launcher::TextInputDialog::DialogLineEdit::DialogLineEdit (QWidget *parent) : - LineEdit (parent) -{ - int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); - - setObjectName(QString("LineEdit")); - setStyleSheet(QString("LineEdit { padding-right: %1px; } ").arg(mClearButton->sizeHint().width() + frameWidth + 1)); - QSize msz = minimumSizeHint(); - setMinimumSize(qMax(msz.width(), mClearButton->sizeHint().height() + frameWidth * 2 + 2), - qMax(msz.height(), mClearButton->sizeHint().height() + frameWidth * 2 + 2)); - -} diff --git a/apps/launcher/utils/textinputdialog.hpp b/apps/launcher/utils/textinputdialog.hpp index bb01778be3..9eb9c717dd 100644 --- a/apps/launcher/utils/textinputdialog.hpp +++ b/apps/launcher/utils/textinputdialog.hpp @@ -13,26 +13,20 @@ namespace Launcher { Q_OBJECT - class DialogLineEdit : public LineEdit - { - public: - explicit DialogLineEdit (QWidget *parent = 0); - }; - - DialogLineEdit *mLineEdit; - QDialogButtonBox *mButtonBox; - public: explicit TextInputDialog(const QString& title, const QString &text, QWidget *parent = 0); - ~TextInputDialog () {} + ~TextInputDialog (); - QString getText() const; + inline LineEdit *lineEdit() { return mLineEdit; } + void setOkButtonEnabled(bool enabled); int exec(); - private slots: - void slotUpdateOkButton(QString text); + private: + + QDialogButtonBox *mButtonBox; + LineEdit *mLineEdit; }; } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 11e44c6b06..0a53e10c50 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -211,8 +211,8 @@ void Wizard::MainWizard::runSettingsImporter() arguments.append(QLatin1String("--cfg")); arguments.append(userPath + QLatin1String("openmw.cfg")); - if (!ProcessInvoker::startProcess(QLatin1String("mwiniimport"), arguments, false)) - return qApp->quit();; +// if (!ProcessInvoker::startProcess(QLatin1String("mwiniimport"), arguments, false)) +// return qApp->quit();; // Re-read the game settings setupGameSettings(); diff --git a/components/config/launchersettings.cpp b/components/config/launchersettings.cpp index 14d66bba79..c014579dc5 100644 --- a/components/config/launchersettings.cpp +++ b/components/config/launchersettings.cpp @@ -41,14 +41,20 @@ QStringList Config::LauncherSettings::subKeys(const QString &key) QMap settings = SettingsBase::getSettings(); QStringList keys = settings.uniqueKeys(); + qDebug() << keys; + QRegExp keyRe("(.+)/"); QStringList result; foreach (const QString ¤tKey, keys) { - if (keyRe.indexIn(currentKey) != -1) { + + if (keyRe.indexIn(currentKey) != -1) + { QString prefixedKey = keyRe.cap(1); - if(prefixedKey.startsWith(key)) { + + if(prefixedKey.startsWith(key)) + { QString subKey = prefixedKey.remove(key); if (!subKey.isEmpty()) result.append(subKey); diff --git a/components/process/processinvoker.cpp b/components/process/processinvoker.cpp index e75daae9b3..7ac8d3c93a 100644 --- a/components/process/processinvoker.cpp +++ b/components/process/processinvoker.cpp @@ -1,23 +1,63 @@ #include "processinvoker.hpp" #include -#include #include #include #include #include #include +#include -Process::ProcessInvoker::ProcessInvoker() +Process::ProcessInvoker::ProcessInvoker(QWidget *parent) { + mProcess = new QProcess(this); + + mName = QString(); + mArguments = QStringList(); } Process::ProcessInvoker::~ProcessInvoker() { } +//void Process::ProcessInvoker::setProcessName(const QString &name) +//{ +// mName = name; +//} + +//void Process::ProcessInvoker::setProcessArguments(const QStringList &arguments) +//{ +// mArguments = arguments; +//} + +QProcess* Process::ProcessInvoker::getProcess() +{ + return mProcess; +} + +//QString Process::ProcessInvoker::getProcessName() +//{ +// return mName; +//} + +//QStringList Process::ProcessInvoker::getProcessArguments() +//{ +// return mArguments; +//} + bool Process::ProcessInvoker::startProcess(const QString &name, const QStringList &arguments, bool detached) { +// mProcess = new QProcess(this); + + connect(mProcess, SIGNAL(error(QProcess::ProcessError)), + this, SLOT(processError(QProcess::ProcessError))); + + connect(mProcess, SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(processFinished(int,QProcess::ExitStatus))); + + mName = name; + mArguments = arguments; + QString path(name); #ifdef Q_OS_WIN path.append(QLatin1String(".exe")); @@ -28,7 +68,6 @@ bool Process::ProcessInvoker::startProcess(const QString &name, const QStringLis path.prepend(QLatin1String("./")); #endif - QProcess process; QFileInfo info(path); if (!info.exists()) { @@ -57,7 +96,7 @@ bool Process::ProcessInvoker::startProcess(const QString &name, const QStringLis // Start the executable if (detached) { - if (!process.startDetached(path, arguments)) { + if (!mProcess->startDetached(path, arguments)) { QMessageBox msgBox; msgBox.setWindowTitle(tr("Error starting executable")); msgBox.setIcon(QMessageBox::Critical); @@ -65,45 +104,79 @@ bool Process::ProcessInvoker::startProcess(const QString &name, const QStringLis msgBox.setText(tr("

Could not start %1

\

An error occurred while starting %1.

\

Press \"Show Details...\" for more information.

").arg(info.fileName())); - msgBox.setDetailedText(process.errorString()); + msgBox.setDetailedText(mProcess->errorString()); msgBox.exec(); return false; } } else { - process.start(path, arguments); - if (!process.waitForFinished()) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error starting executable")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("

Could not start %1

\ -

An error occurred while starting %1.

\ -

Press \"Show Details...\" for more information.

").arg(info.fileName())); - msgBox.setDetailedText(process.errorString()); - msgBox.exec(); + mProcess->start(path, arguments); - return false; - } +// if (!mProcess->waitForFinished()) { +// QMessageBox msgBox; +// msgBox.setWindowTitle(tr("Error starting executable")); +// msgBox.setIcon(QMessageBox::Critical); +// msgBox.setStandardButtons(QMessageBox::Ok); +// msgBox.setText(tr("

Could not start %1

\ +//

An error occurred while starting %1.

\ +//

Press \"Show Details...\" for more information.

").arg(info.fileName())); +// msgBox.setDetailedText(mProcess->errorString()); +// msgBox.exec(); - if (process.exitCode() != 0 || process.exitStatus() == QProcess::CrashExit) { - QString error(process.readAllStandardError()); - error.append(tr("\nArguments:\n")); - error.append(arguments.join(" ")); +// return false; +// } - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error running executable")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("

Executable %1 returned an error

\ -

An error occurred while running %1.

\ -

Press \"Show Details...\" for more information.

").arg(info.fileName())); - msgBox.setDetailedText(error); - msgBox.exec(); +// if (mProcess->exitCode() != 0 || mProcess->exitStatus() == QProcess::CrashExit) { +// QString error(mProcess->readAllStandardError()); +// error.append(tr("\nArguments:\n")); +// error.append(arguments.join(" ")); - return false; - } +// QMessageBox msgBox; +// msgBox.setWindowTitle(tr("Error running executable")); +// msgBox.setIcon(QMessageBox::Critical); +// msgBox.setStandardButtons(QMessageBox::Ok); +// msgBox.setText(tr("

Executable %1 returned an error

\ +//

An error occurred while running %1.

\ +//

Press \"Show Details...\" for more information.

").arg(info.fileName())); +// msgBox.setDetailedText(error); +// msgBox.exec(); + +// return false; +// } } return true; } + +void Process::ProcessInvoker::processError(QProcess::ProcessError error) +{ + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error running executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Executable %1 returned an error

\ +

An error occurred while running %1.

\ +

Press \"Show Details...\" for more information.

").arg(mName)); + msgBox.setDetailedText(mProcess->errorString()); + msgBox.exec(); + +} + +void Process::ProcessInvoker::processFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + if (exitCode != 0 || exitStatus == QProcess::CrashExit) { + QString error(mProcess->readAllStandardError()); + error.append(tr("\nArguments:\n")); + error.append(mArguments.join(" ")); + + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error running executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Executable %1 returned an error

\ +

An error occurred while running %1.

\ +

Press \"Show Details...\" for more information.

").arg(mName)); + msgBox.setDetailedText(error); + msgBox.exec(); + } +} diff --git a/components/process/processinvoker.hpp b/components/process/processinvoker.hpp index d59d2f0121..07907e3b4c 100644 --- a/components/process/processinvoker.hpp +++ b/components/process/processinvoker.hpp @@ -3,6 +3,7 @@ #include #include +#include namespace Process { @@ -10,13 +11,32 @@ namespace Process { Q_OBJECT - ProcessInvoker(); - ~ProcessInvoker(); - public: - inline static bool startProcess(const QString &name, bool detached = false) { return startProcess(name, QStringList(), detached); } - bool static startProcess(const QString &name, const QStringList &arguments, bool detached = false); + ProcessInvoker(QWidget *parent = 0); + ~ProcessInvoker(); + +// void setProcessName(const QString &name); +// void setProcessArguments(const QStringList &arguments); + + QProcess* getProcess(); +// QString getProcessName(); +// QStringList getProcessArguments(); + +// inline bool startProcess(bool detached = false) { return startProcess(mName, mArguments, detached); } + inline bool startProcess(const QString &name, bool detached = false) { return startProcess(name, QStringList(), detached); } + bool startProcess(const QString &name, const QStringList &arguments, bool detached = false); + + private: + QProcess *mProcess; + + QString mName; + QStringList mArguments; + + private slots: + void processError(QProcess::ProcessError error); + void processFinished(int exitCode, QProcess::ExitStatus exitStatus); + }; } diff --git a/files/ui/settingspage.ui b/files/ui/settingspage.ui index ce5aa57ece..6c873ea926 100644 --- a/files/ui/settingspage.ui +++ b/files/ui/settingspage.ui @@ -6,8 +6,8 @@ 0 0 - 516 - 399 + 514 + 397
@@ -88,13 +88,7 @@
- - - - /home/user/.local/share/openmw/data/Morrowind.ini - - - + From f8bb797b8a384f33e44436a58184382872b83cbf Mon Sep 17 00:00:00 2001 From: pvdk Date: Wed, 16 Apr 2014 18:34:24 +0200 Subject: [PATCH 050/303] Settings tab is fully functional now --- apps/launcher/datafilespage.cpp | 30 +++---- apps/launcher/settingspage.cpp | 107 ++++++++++++++++------- apps/launcher/settingspage.hpp | 1 + apps/wizard/existinginstallationpage.cpp | 5 +- apps/wizard/languageselectionpage.cpp | 14 +-- 5 files changed, 104 insertions(+), 53 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index efd9765aca..72afe1c1db 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -42,7 +42,7 @@ void Launcher::DataFilesPage::loadSettings() QString profileName = ui.profilesComboBox->currentText(); - QStringList files = mLauncherSettings.values(QString("Profiles/") + profileName, Qt::MatchExactly); + QStringList files = mLauncherSettings.values(QString("Profiles/") + profileName + QString("/content"), Qt::MatchExactly); QStringList filepaths; @@ -75,7 +75,7 @@ void Launcher::DataFilesPage::saveSettings(const QString &profile) mLauncherSettings.setValue(QString("Profiles/currentprofile"), ui.profilesComboBox->currentText()); foreach(const ContentSelectorModel::EsmFile *item, items) { - mLauncherSettings.setMultiValue(QString("Profiles/") + profileName, item->fileName()); + mLauncherSettings.setMultiValue(QString("Profiles/") + profileName + QString("/content"), item->fileName()); mGameSettings.setMultiValue(QString("content"), item->fileName()); } @@ -192,21 +192,21 @@ void Launcher::DataFilesPage::setupDataFiles() if (!mDataLocal.isEmpty()) mSelector->addFiles(mDataLocal); - QStringList profiles; + QStringList profiles = mLauncherSettings.subKeys(QString("Profiles/")); QString currentProfile = mLauncherSettings.getSettings().value("Profiles/currentprofile"); - foreach (QString key, mLauncherSettings.getSettings().keys()) - { - if (key.contains("Profiles/")) - { - QString profile = key.mid (9); - if (profile != "currentprofile") - { - if (!profiles.contains(profile)) - profiles << profile; - } - } - } +// foreach (QString key, mLauncherSettings.getSettings().keys()) +// { +// if (key.contains("Profiles/")) +// { +// QString profile = key.mid (9); +// if (profile != "currentprofile") +// { +// if (!profiles.contains(profile)) +// profiles << profile; +// } +// } +// } foreach (const QString &item, profiles) addProfile (item, false); diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index b2d03ba952..3c8c119ec6 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -1,7 +1,9 @@ #include "settingspage.hpp" +#include #include #include +#include #include #include @@ -20,13 +22,13 @@ Launcher::SettingsPage::SettingsPage(Config::GameSettings &gameSettings, setupUi(this); QStringList languages; - languages << "English" - << "French" - << "German" - << "Italian" - << "Polish" - << "Russian" - << "Spanish"; + languages << QLatin1String("English") + << QLatin1String("French") + << QLatin1String("German") + << QLatin1String("Italian") + << QLatin1String("Polish") + << QLatin1String("Russian") + << QLatin1String("Spanish"); languageComboBox->addItems(languages); @@ -50,22 +52,31 @@ Launcher::SettingsPage::SettingsPage(Config::GameSettings &gameSettings, connect(mProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(updateOkButton(QString))); -// // Detect Morrowind configuration files -// foreach (const QString &path, mGameSettings.getDataDirs()) { -// QDir dir(path); -// dir.setPath(dir.canonicalPath()); // Resolve symlinks + // Detect Morrowind configuration files + QStringList iniPaths; -// if (dir.exists(QString("Morrowind.ini"))) -// iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); -// else -// { -// if (!dir.cdUp()) -// continue; // Cannot move from Data Files + foreach (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"))); -// } -// } + 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); + } } void Launcher::SettingsPage::on_wizardButton_clicked() @@ -80,6 +91,32 @@ void Launcher::SettingsPage::on_importerButton_clicked() return; } +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() { qDebug() << "wizard started!"; @@ -109,18 +146,27 @@ void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus if (exitCode != 0 || exitStatus == QProcess::CrashExit) return; - mMain->writeSettings(); - mMain->reloadSettings(); + // Import selected data files from openmw.cfg + if (addonsCheckBox->isChecked()) + { + if (mProfileDialog->exec() == QDialog::Accepted) + { + const QString profile(mProfileDialog->lineEdit()->text()); + const QStringList files(mGameSettings.values(QLatin1String("content"))); + // Doesn't quite work right now + mLauncherSettings.setValue(QLatin1String("Profiles/currentprofile"), profile); - if (addonsCheckBox->isChecked()) { + foreach (const QString &file, files) { + mLauncherSettings.setMultiValue(QLatin1String("Profiles/") + profile + QLatin1String("/content"), file); + } - if (mProfileDialog->exec() == QDialog::Accepted) { - QString profile = mProfileDialog->lineEdit()->text(); - qDebug() << profile; + mGameSettings.remove(QLatin1String("content")); } } + mMain->writeSettings(); + mMain->reloadSettings(); importerButton->setEnabled(true); } @@ -132,8 +178,9 @@ void Launcher::SettingsPage::updateOkButton(const QString &text) return; } + const QStringList profiles(mLauncherSettings.subKeys(QString("Profiles/"))); -// (profilesComboBox->findText(text) == -1) -// ? mNewProfileDialog->setOkButtonEnabled(true) -// : mNewProfileDialog->setOkButtonEnabled(false); + (profiles.contains(text)) + ? mProfileDialog->setOkButtonEnabled(false) + : mProfileDialog->setOkButtonEnabled(true); } diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index ed5c068fb1..d43141fc4e 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -31,6 +31,7 @@ namespace Launcher private slots: void on_wizardButton_clicked(); void on_importerButton_clicked(); + void on_browseButton_clicked(); void wizardStarted(); void wizardFinished(int exitCode, QProcess::ExitStatus exitStatus); diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index d37dd949d6..13dcf9941c 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -72,7 +72,7 @@ bool Wizard::ExistingInstallationPage::validatePage() QString iniFile; if (msgBox.clickedButton() == browseButton) { iniFile = QFileDialog::getOpenFileName( - NULL, + this, QObject::tr("Select configuration file"), QDir::currentPath(), QString(tr("Morrowind configuration file (*.ini)"))); @@ -100,6 +100,9 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() NULL, QFileDialog::DontResolveSymlinks); + if (selectedFile.isEmpty()) + return; + QFileInfo info(selectedFile); if (!info.exists()) diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp index b921d5ddbc..8beb106a28 100644 --- a/apps/wizard/languageselectionpage.cpp +++ b/apps/wizard/languageselectionpage.cpp @@ -14,13 +14,13 @@ Wizard::LanguageSelectionPage::LanguageSelectionPage(MainWizard *wizard) : void Wizard::LanguageSelectionPage::initializePage() { QStringList languages; - languages << "English" - << "French" - << "German" - << "Italian" - << "Polish" - << "Russian" - << "Spanish"; + languages << QLatin1String("English") + << QLatin1String("French") + << QLatin1String("German") + << QLatin1String("Italian") + << QLatin1String("Polish") + << QLatin1String("Russian") + << QLatin1String("Spanish"); languageComboBox->addItems(languages); } From 6ed76858d9ce85f9ead853bab070ed48cb48574d Mon Sep 17 00:00:00 2001 From: pvdk Date: Wed, 16 Apr 2014 23:59:25 +0200 Subject: [PATCH 051/303] Fixed some minor problems with the wizard --- apps/wizard/existinginstallationpage.cpp | 48 +++++---------- apps/wizard/existinginstallationpage.hpp | 5 -- apps/wizard/languageselectionpage.cpp | 15 ++++- apps/wizard/mainwizard.cpp | 76 +++++++++++++++++++++++- apps/wizard/mainwizard.hpp | 3 + 5 files changed, 106 insertions(+), 41 deletions(-) diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 13dcf9941c..66edc1e8a6 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -14,36 +14,30 @@ Wizard::ExistingInstallationPage::ExistingInstallationPage(MainWizard *wizard) : { setupUi(this); - mEmptyItem = new QListWidgetItem(tr("No existing installations detected")); - mEmptyItem->setFlags(Qt::NoItemFlags); - installationsList->addItem(mEmptyItem); - mEmptyItem->setHidden(true); + QListWidgetItem *emptyItem = new QListWidgetItem(tr("No existing installations detected")); + emptyItem->setFlags(Qt::NoItemFlags); + installationsList->addItem(emptyItem); - connect(installationsList, SIGNAL(currentTextChanged(QString)), - this, SLOT(textChanged(QString))); - - connect(installationsList,SIGNAL(itemSelectionChanged()), - this, SIGNAL(completeChanged())); -} - -void Wizard::ExistingInstallationPage::initializePage() -{ + // Add the available installation paths QStringList paths(mWizard->mInstallations.keys()); + // Hide the default item if there are installations to choose from if (paths.isEmpty()) { - mEmptyItem->setHidden(false); - return; + installationsList->item(0)->setHidden(false); + } else { + installationsList->item(0)->setHidden(true); } - // Make to clear list before adding items - // to prevent duplicates when going back and forth between pages - installationsList->clear(); - foreach (const QString &path, paths) { QListWidgetItem *item = new QListWidgetItem(path); installationsList->addItem(item); } + connect(installationsList, SIGNAL(currentTextChanged(QString)), + this, SLOT(textChanged(QString))); + + connect(installationsList,SIGNAL(itemSelectionChanged()), + this, SIGNAL(completeChanged())); } bool Wizard::ExistingInstallationPage::validatePage() @@ -119,7 +113,7 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() mWizard->addInstallation(path); // Hide the default item - mEmptyItem->setHidden(true); + installationsList->item(0)->setHidden(true); QListWidgetItem *item = new QListWidgetItem(path); installationsList->addItem(item); @@ -149,17 +143,5 @@ bool Wizard::ExistingInstallationPage::isComplete() const int Wizard::ExistingInstallationPage::nextId() const { - QString path(field(QLatin1String("installation.path")).toString()); - - if (path.isEmpty()) - return MainWizard::Page_LanguageSelection; - - if (mWizard->mInstallations[path]->hasMorrowind == true && - mWizard->mInstallations[path]->hasTribunal == true && - mWizard->mInstallations[path]->hasBloodmoon == true) - { - return MainWizard::Page_Import; - } else { - return MainWizard::Page_LanguageSelection; - } + return MainWizard::Page_LanguageSelection; } diff --git a/apps/wizard/existinginstallationpage.hpp b/apps/wizard/existinginstallationpage.hpp index 04385893ac..54ee8d2e2f 100644 --- a/apps/wizard/existinginstallationpage.hpp +++ b/apps/wizard/existinginstallationpage.hpp @@ -2,7 +2,6 @@ #define EXISTINGINSTALLATIONPAGE_HPP #include -#include #include "ui_existinginstallationpage.h" @@ -27,11 +26,7 @@ namespace Wizard private: MainWizard *mWizard; - QListWidgetItem *mEmptyItem; - - protected: - void initializePage(); }; } diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp index 8beb106a28..7f97e69183 100644 --- a/apps/wizard/languageselectionpage.cpp +++ b/apps/wizard/languageselectionpage.cpp @@ -27,5 +27,18 @@ void Wizard::LanguageSelectionPage::initializePage() int Wizard::LanguageSelectionPage::nextId() const { - return MainWizard::Page_ComponentSelection; + // Check if we have to install something + QString path(field(QLatin1String("installation.path")).toString()); + + if (path.isEmpty()) + return MainWizard::Page_ComponentSelection; + + if (mWizard->mInstallations[path]->hasMorrowind == true && + mWizard->mInstallations[path]->hasTribunal == true && + mWizard->mInstallations[path]->hasBloodmoon == true) + { + return MainWizard::Page_Import; + } else { + return MainWizard::Page_ComponentSelection; + } } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 0a53e10c50..fa27b548c4 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -44,6 +44,7 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setupLog(); setupGameSettings(); + setupLauncherSettings(); setupInstallations(); setupPages(); } @@ -149,6 +150,40 @@ void Wizard::MainWizard::setupGameSettings() } } +void Wizard::MainWizard::setupLauncherSettings() +{ + QString path(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); + path.append(QLatin1String("launcher.cfg")); + + QString message(tr("

Could not open %1 for reading

\ +

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

")); + + + QFile file(path); + + qDebug() << "Loading config file:" << qPrintable(path); + + if (file.exists()) { + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(message.arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + QTextStream stream(&file); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + + mLauncherSettings.readFile(stream); + } + + file.close(); + +} + void Wizard::MainWizard::setupInstallations() { // Check if the paths actually contain a Morrowind installation @@ -211,8 +246,10 @@ void Wizard::MainWizard::runSettingsImporter() arguments.append(QLatin1String("--cfg")); arguments.append(userPath + QLatin1String("openmw.cfg")); -// if (!ProcessInvoker::startProcess(QLatin1String("mwiniimport"), arguments, false)) -// return qApp->quit();; + ProcessInvoker invoker(this); + + if (!invoker.startProcess(QLatin1String("mwiniimport"), arguments, false)) + return qApp->quit();; // Re-read the game settings setupGameSettings(); @@ -285,6 +322,19 @@ void Wizard::MainWizard::reject() void Wizard::MainWizard::writeSettings() { + // Write the encoding and language settings + QString language(field(QLatin1String("installation.language")).toString()); + 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")); + } + + // Write the installation path so that openmw can find them QString path(field(QLatin1String("installation.path")).toString()); // Make sure the installation path is the last data= entry @@ -329,6 +379,28 @@ void Wizard::MainWizard::writeSettings() mGameSettings.writeFile(stream); file.close(); + + // Launcher settings + file.setFileName(userPath + QLatin1String("launcher.cfg")); + + if (!file.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) { + // File cannot be opened or 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 %1 for writing

\ +

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

").arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + + stream.setDevice(&file); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + + mLauncherSettings.writeFile(stream); + file.close(); } bool Wizard::MainWizard::findFiles(const QString &name, const QString &path) diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index dcb07a8c2b..63b7e62e8b 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -6,6 +6,7 @@ #include #include +#include namespace Wizard { @@ -53,12 +54,14 @@ namespace Wizard void setupLog(); void setupGameSettings(); + void setupLauncherSettings(); void setupInstallations(); void setupPages(); void writeSettings(); Config::GameSettings mGameSettings; + Config::LauncherSettings mLauncherSettings; QTextStream *mLog; From 30c3c3e245c8b19a18ce08df208d24e96594f480 Mon Sep 17 00:00:00 2001 From: pvdk Date: Thu, 17 Apr 2014 00:01:19 +0200 Subject: [PATCH 052/303] Working on importing content lists in the launcher --- apps/launcher/datafilespage.cpp | 126 ++++++------ apps/launcher/datafilespage.hpp | 5 +- apps/launcher/maindialog.cpp | 245 ++++++++++-------------- apps/launcher/settingspage.cpp | 95 ++++++++- apps/launcher/settingspage.hpp | 7 +- apps/launcher/utils/textinputdialog.cpp | 2 +- 6 files changed, 257 insertions(+), 223 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 72afe1c1db..21652a8f40 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -34,16 +34,68 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: setupDataFiles(); } -void Launcher::DataFilesPage::loadSettings() +void Launcher::DataFilesPage::buildView() +{ + ui.verticalLayout->insertWidget (0, mSelector->uiWidget()); + + //tool buttons + ui.newProfileButton->setToolTip ("Create a new profile"); + ui.deleteProfileButton->setToolTip ("Delete an existing profile"); + + //combo box + ui.profilesComboBox->addItem ("Default"); + ui.profilesComboBox->setPlaceholderText (QString("Select a profile...")); + ui.profilesComboBox->setCurrentIndex(ui.profilesComboBox->findText(QLatin1String("Default"))); + + // Add the actions to the toolbuttons + ui.newProfileButton->setDefaultAction (ui.newProfileAction); + ui.deleteProfileButton->setDefaultAction (ui.deleteProfileAction); + + //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))); +} + +void Launcher::DataFilesPage::setupDataFiles() +{ + QStringList paths = mGameSettings.getDataDirs(); + + foreach (const QString &path, paths) + mSelector->addFiles(path); + + mDataLocal = mGameSettings.getDataLocal(); + + if (!mDataLocal.isEmpty()) + mSelector->addFiles(mDataLocal); + + loadSettings(); +} + +bool Launcher::DataFilesPage::loadSettings() { QStringList paths = mGameSettings.getDataDirs(); paths.insert (0, mDataLocal); PathIterator pathIterator (paths); - QString profileName = ui.profilesComboBox->currentText(); + QStringList profiles = mLauncherSettings.subKeys(QString("Profiles/")); + QString currentProfile = mLauncherSettings.getSettings().value("Profiles/currentprofile"); - QStringList files = mLauncherSettings.values(QString("Profiles/") + profileName + QString("/content"), Qt::MatchExactly); + qDebug() << "current profile is: " << currentProfile; + foreach (const QString &item, profiles) + addProfile (item, false); + + // Hack: also add the current profile + if (!currentProfile.isEmpty()) + addProfile(currentProfile, true); + + QStringList files = mLauncherSettings.values(QString("Profiles/") + currentProfile + QString("/content"), Qt::MatchExactly); QStringList filepaths; foreach (const QString &file, files) @@ -55,6 +107,8 @@ void Launcher::DataFilesPage::loadSettings() } mSelector->setProfileContent (filepaths); + + return true; } void Launcher::DataFilesPage::saveSettings(const QString &profile) @@ -81,33 +135,6 @@ void Launcher::DataFilesPage::saveSettings(const QString &profile) } -void Launcher::DataFilesPage::buildView() -{ - ui.verticalLayout->insertWidget (0, mSelector->uiWidget()); - - //tool buttons - ui.newProfileButton->setToolTip ("Create a new profile"); - ui.deleteProfileButton->setToolTip ("Delete an existing profile"); - - //combo box - ui.profilesComboBox->addItem ("Default"); - ui.profilesComboBox->setPlaceholderText (QString("Select a profile...")); - - // Add the actions to the toolbuttons - ui.newProfileButton->setDefaultAction (ui.newProfileAction); - ui.deleteProfileButton->setDefaultAction (ui.deleteProfileAction); - - //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))); -} - void Launcher::DataFilesPage::removeProfile(const QString &profile) { mLauncherSettings.remove(QString("Profiles/") + profile); @@ -140,6 +167,9 @@ void Launcher::DataFilesPage::setProfile (const QString &previous, const QString if (previous == current) return; + if (previous.isEmpty()) + return; + if (!previous.isEmpty() && savePrevious) saveSettings (previous); @@ -180,42 +210,6 @@ void Launcher::DataFilesPage::slotProfileChanged(int index) setProfile (index, true); } -void Launcher::DataFilesPage::setupDataFiles() -{ - QStringList paths = mGameSettings.getDataDirs(); - - foreach (const QString &path, paths) - mSelector->addFiles(path); - - mDataLocal = mGameSettings.getDataLocal(); - - if (!mDataLocal.isEmpty()) - mSelector->addFiles(mDataLocal); - - QStringList profiles = mLauncherSettings.subKeys(QString("Profiles/")); - QString currentProfile = mLauncherSettings.getSettings().value("Profiles/currentprofile"); - -// foreach (QString key, mLauncherSettings.getSettings().keys()) -// { -// if (key.contains("Profiles/")) -// { -// QString profile = key.mid (9); -// if (profile != "currentprofile") -// { -// if (!profiles.contains(profile)) -// profiles << profile; -// } -// } -// } - - foreach (const QString &item, profiles) - addProfile (item, false); - - setProfile (ui.profilesComboBox->findText(currentProfile), false); - - loadSettings(); -} - void Launcher::DataFilesPage::on_newProfileAction_triggered() { TextInputDialog newDialog (tr("New Profile"), tr("Profile name:"), this); diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index 642668cb74..06efdfca3b 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -39,7 +39,9 @@ namespace Launcher //void writeConfig(QString profile = QString()); void saveSettings(const QString &profile = ""); - void loadSettings(); + bool loadSettings(); + + void setupDataFiles(); signals: void signalProfileChanged (int index); @@ -70,7 +72,6 @@ namespace Launcher void setPluginsCheckstates(Qt::CheckState state); void buildView(); - void setupDataFiles(); void setupConfig(); void readConfig(); void setProfile (int index, bool savePrevious); diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index b8f9efd1af..9c6e33e1b1 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -137,7 +137,7 @@ void Launcher::MainDialog::createPages() mPlayPage = new PlayPage(this); mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this); mGraphicsPage = new GraphicsPage(mCfgMgr, mGraphicsSettings, this); - mSettingsPage = new SettingsPage(mGameSettings, mLauncherSettings, this); + mSettingsPage = new SettingsPage(mCfgMgr, mGameSettings, mLauncherSettings, this); // Set the combobox of the play page to imitate the combobox on the datafilespage mPlayPage->setProfilesModel(mDataFilesPage->profilesModel()); @@ -161,150 +161,103 @@ void Launcher::MainDialog::createPages() bool Launcher::MainDialog::showFirstRunDialog() { - QStringList iniPaths; - - foreach (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"))); - } - } - - // Ask the user where the Morrowind.ini is - if (iniPaths.empty()) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error detecting Morrowind configuration")); - msgBox.setIcon(QMessageBox::Warning); - msgBox.setStandardButtons(QMessageBox::Cancel); - msgBox.setText(QObject::tr("
Could not find Morrowind.ini

\ - OpenMW needs to import settings from this file.

\ - Press \"Browse...\" to specify the location manually.
")); - - QAbstractButton *dirSelectButton = - msgBox.addButton(QObject::tr("B&rowse..."), QMessageBox::ActionRole); - - msgBox.exec(); - - QString iniFile; - if (msgBox.clickedButton() == dirSelectButton) { - iniFile = QFileDialog::getOpenFileName( - NULL, - QObject::tr("Select configuration file"), - QDir::currentPath(), - QString(tr("Morrowind configuration file (*.ini)"))); - } - - if (iniFile.isEmpty()) - return false; // Cancel was clicked; - - QFileInfo info(iniFile); - iniPaths.clear(); - iniPaths.append(info.absoluteFilePath()); - } - - CheckableMessageBox msgBox(this); - msgBox.setWindowTitle(tr("Morrowind installation detected")); - - QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion); - int size = QApplication::style()->pixelMetric(QStyle::PM_MessageBoxIconSize); - msgBox.setIconPixmap(icon.pixmap(size, size)); - - QAbstractButton *importerButton = - msgBox.addButton(tr("Import"), QDialogButtonBox::AcceptRole); // ActionRole doesn't work?! - QAbstractButton *skipButton = - msgBox.addButton(tr("Skip"), QDialogButtonBox::RejectRole); - - Q_UNUSED(skipButton); // Surpress compiler unused warning - - msgBox.setStandardButtons(QDialogButtonBox::NoButton); - msgBox.setText(tr("
An existing Morrowind configuration was detected
\ -
Would you like to import settings from Morrowind.ini?
\ -
Warning: In most cases OpenMW needs these settings to run properly
")); - msgBox.setCheckBoxText(tr("Include selected masters and plugins (creates a new profile)")); - msgBox.exec(); - if (msgBox.clickedButton() == importerButton) { - if (iniPaths.count() > 1) { - // Multiple Morrowind.ini files found - bool ok; - QString path = QInputDialog::getItem(this, tr("Multiple configurations found"), - tr("
There are multiple Morrowind.ini files found.

\ - Please select the one you wish to import from:"), iniPaths, 0, false, &ok); - if (ok && !path.isEmpty()) { - iniPaths.clear(); - iniPaths.append(path); - } else { - // Cancel was clicked - return false; - } - } +// CheckableMessageBox msgBox(this); +// msgBox.setWindowTitle(tr("Morrowind installation detected")); - // Create the file if it doesn't already exist, else the importer will fail - QString path = QString::fromStdString(mCfgMgr.getUserConfigPath().string()) + QString("openmw.cfg"); - QFile file(path); +// QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion); +// int size = QApplication::style()->pixelMetric(QStyle::PM_MessageBoxIconSize); +// msgBox.setIconPixmap(icon.pixmap(size, size)); - 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 %0 for writing

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); - msgBox.exec(); - return false; - } +// QAbstractButton *importerButton = +// msgBox.addButton(tr("Import"), QDialogButtonBox::AcceptRole); // ActionRole doesn't work?! +// QAbstractButton *skipButton = +// msgBox.addButton(tr("Skip"), QDialogButtonBox::RejectRole); - file.close(); - } +// Q_UNUSED(skipButton); // Surpress compiler unused warning - // Construct the arguments to run the importer - QStringList arguments; +// msgBox.setStandardButtons(QDialogButtonBox::NoButton); +// msgBox.setText(tr("
An existing Morrowind configuration was detected
\ +//
Would you like to import settings from Morrowind.ini?
\ +//
Warning: In most cases OpenMW needs these settings to run properly
")); +// msgBox.setCheckBoxText(tr("Include selected masters and plugins (creates a new profile)")); +// msgBox.exec(); - if (msgBox.isChecked()) - arguments.append(QString("--game-files")); - arguments.append(QString("--encoding")); - arguments.append(mGameSettings.value(QString("encoding"), QString("win1252"))); - arguments.append(QString("--ini")); - arguments.append(iniPaths.first()); - arguments.append(QString("--cfg")); - arguments.append(path); +// if (msgBox.clickedButton() == importerButton) { - ProcessInvoker invoker(this); +// if (iniPaths.count() > 1) { +// // Multiple Morrowind.ini files found +// bool ok; +// QString path = QInputDialog::getItem(this, tr("Multiple configurations found"), +// tr("
There are multiple Morrowind.ini files found.

\ +// Please select the one you wish to import from:"), iniPaths, 0, false, &ok); +// if (ok && !path.isEmpty()) { +// iniPaths.clear(); +// iniPaths.append(path); +// } else { +// // Cancel was clicked +// return false; +// } +// } - if (!invoker.startProcess(QLatin1String("mwiniimport"), arguments, false)) - return false; +// // Create the file if it doesn't already exist, else the importer will fail +// QString path = QString::fromStdString(mCfgMgr.getUserConfigPath().string()) + QString("openmw.cfg"); +// QFile file(path); - // Re-read the game settings - if (!setupGameSettings()) - return false; +// 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 %0 for writing

\ +// Please make sure you have the right permissions \ +// and try again.
").arg(file.fileName())); +// msgBox.exec(); +// return false; +// } - // Add a new profile - if (msgBox.isChecked()) { - mLauncherSettings.setValue(QString("Profiles/currentprofile"), QString("Imported")); - mLauncherSettings.remove(QString("Profiles/Imported/content")); +// file.close(); +// } - QStringList contents = mGameSettings.values(QString("content")); - foreach (const QString &content, contents) { - mLauncherSettings.setMultiValue(QString("Profiles/Imported/content"), content); - } - } +// // Construct the arguments to run the importer +// QStringList arguments; - } +// if (msgBox.isChecked()) +// arguments.append(QString("--game-files")); + +// arguments.append(QString("--encoding")); +// arguments.append(mGameSettings.value(QString("encoding"), QString("win1252"))); +// arguments.append(QString("--ini")); +// arguments.append(iniPaths.first()); +// arguments.append(QString("--cfg")); +// arguments.append(path); + +// ProcessInvoker invoker(this); + +// if (!invoker.startProcess(QLatin1String("mwiniimport"), arguments, false)) +// return false; + +// // Re-read the game settings +// if (!setupGameSettings()) +// return false; + +// // Add a new profile +// if (msgBox.isChecked()) { +// mLauncherSettings.setValue(QString("Profiles/currentprofile"), QString("Imported")); +// mLauncherSettings.remove(QString("Profiles/Imported/content")); + +// QStringList contents = mGameSettings.values(QString("content")); +// foreach (const QString &content, contents) { +// mLauncherSettings.setMultiValue(QString("Profiles/Imported/content"), content); +// } +// } + +// } return true; } @@ -349,8 +302,11 @@ bool Launcher::MainDialog::reloadSettings() if (!setupGraphicsSettings()) return false; -// if (!mSettingsPage->loadSettings()) -// return false; + if (!mSettingsPage->loadSettings()) + return false; + + if (!mDataFilesPage->loadSettings()) + return false; if (!mGraphicsPage->loadSettings()) return false; @@ -371,17 +327,17 @@ void Launcher::MainDialog::changePage(QListWidgetItem *current, QListWidgetItem DataFilesPage *previousPage = dynamic_cast(pagesWidget->widget(previousIndex)); DataFilesPage *currentPage = dynamic_cast(pagesWidget->widget(currentIndex)); - //special call to update/save data files page list view when it's displayed/hidden. - if (previousPage) - { - if (previousPage->objectName() == "DataFilesPage") - previousPage->saveSettings(); - } - else if (currentPage) - { - if (currentPage->objectName() == "DataFilesPage") - currentPage->loadSettings(); - } +// //special call to update/save data files page list view when it's displayed/hidden. +// if (previousPage) +// { +// if (previousPage->objectName() == "DataFilesPage") +// previousPage->saveSettings(); +// } +// else if (currentPage) +// { +// if (currentPage->objectName() == "DataFilesPage") +// currentPage->loadSettings(); +// } } bool Launcher::MainDialog::setupLauncherSettings() @@ -729,8 +685,9 @@ bool Launcher::MainDialog::writeSettings() { // Now write all config files saveSettings(); - mGraphicsPage->saveSettings(); mDataFilesPage->saveSettings(); + mGraphicsPage->saveSettings(); + mSettingsPage->saveSettings(); QString userPath = QString::fromStdString(mCfgMgr.getUserConfigPath().string()); QDir dir(userPath); diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 3c8c119ec6..f673917fdb 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -5,6 +5,8 @@ #include #include +#include + #include #include @@ -12,12 +14,14 @@ using namespace Process; -Launcher::SettingsPage::SettingsPage(Config::GameSettings &gameSettings, - Config::LauncherSettings &launcherSettings, MainDialog *parent) : - mGameSettings(gameSettings), - mLauncherSettings(launcherSettings), - QWidget(parent), - mMain(parent) +Launcher::SettingsPage::SettingsPage(Files::ConfigurationManager &cfg, + Config::GameSettings &gameSettings, + Config::LauncherSettings &launcherSettings, MainDialog *parent) + : mCfgMgr(cfg) + , mGameSettings(gameSettings) + , mLauncherSettings(launcherSettings) + , QWidget(parent) + , mMain(parent) { setupUi(this); @@ -77,17 +81,60 @@ Launcher::SettingsPage::SettingsPage(Config::GameSettings &gameSettings, } else { importerButton->setEnabled(false); } + + loadSettings(); } void Launcher::SettingsPage::on_wizardButton_clicked() { + saveSettings(); + if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) return; } void Launcher::SettingsPage::on_importerButton_clicked() { - if (!mImporterInvoker->startProcess(QLatin1String("mwiniimport"), false)) + saveSettings(); + + // 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; + + if (!mImporterInvoker->startProcess(QLatin1String("mwiniimport"), arguments, false)) return; } @@ -129,7 +176,6 @@ void Launcher::SettingsPage::wizardFinished(int exitCode, QProcess::ExitStatus e if (exitCode != 0 || exitStatus == QProcess::CrashExit) return; - mMain->writeSettings(); mMain->reloadSettings(); wizardButton->setEnabled(true); } @@ -146,6 +192,9 @@ void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus if (exitCode != 0 || exitStatus == QProcess::CrashExit) return; + // Re-read the settings in their current state + mMain->reloadSettings(); + // Import selected data files from openmw.cfg if (addonsCheckBox->isChecked()) { @@ -154,6 +203,8 @@ void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus const QString profile(mProfileDialog->lineEdit()->text()); const QStringList files(mGameSettings.values(QLatin1String("content"))); + qDebug() << "Profile " << profile << files; + // Doesn't quite work right now mLauncherSettings.setValue(QLatin1String("Profiles/currentprofile"), profile); @@ -165,7 +216,6 @@ void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus } } - mMain->writeSettings(); mMain->reloadSettings(); importerButton->setEnabled(true); } @@ -184,3 +234,30 @@ void Launcher::SettingsPage::updateOkButton(const QString &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; +} diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index d43141fc4e..846f5681c8 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -10,6 +10,7 @@ #include "maindialog.hpp" +namespace Files { struct ConfigurationManager; } namespace Config { class GameSettings; class LauncherSettings; } @@ -22,13 +23,14 @@ namespace Launcher Q_OBJECT public: - SettingsPage( Config::GameSettings &gameSettings, + SettingsPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, Config::LauncherSettings &launcherSettings, MainDialog *parent = 0); void saveSettings(); bool loadSettings(); private slots: + void on_wizardButton_clicked(); void on_importerButton_clicked(); void on_browseButton_clicked(); @@ -42,9 +44,12 @@ namespace Launcher void updateOkButton(const QString &text); private: + Process::ProcessInvoker *mWizardInvoker; Process::ProcessInvoker *mImporterInvoker; + Files::ConfigurationManager &mCfgMgr; + Config::GameSettings &mGameSettings; Config::LauncherSettings &mLauncherSettings; diff --git a/apps/launcher/utils/textinputdialog.cpp b/apps/launcher/utils/textinputdialog.cpp index 74e40a81c2..b47fdb6e62 100644 --- a/apps/launcher/utils/textinputdialog.cpp +++ b/apps/launcher/utils/textinputdialog.cpp @@ -14,7 +14,7 @@ Launcher::TextInputDialog::TextInputDialog(const QString& title, const QString & mButtonBox = new QDialogButtonBox(this); mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); -// mButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); + mButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); QLabel *label = new QLabel(this); label->setText(text); From 5d94cb112f0574a95f79f2773dadc693f1c5fc7f Mon Sep 17 00:00:00 2001 From: pvdk Date: Thu, 17 Apr 2014 02:15:06 +0200 Subject: [PATCH 053/303] Fixed segfault bug due to accessing members of unitialized struct --- apps/wizard/componentselectionpage.cpp | 12 ++++---- apps/wizard/existinginstallationpage.cpp | 4 +-- apps/wizard/installationpage.cpp | 10 +++---- apps/wizard/languageselectionpage.cpp | 28 +++++++++++------- apps/wizard/mainwizard.cpp | 37 +++++++++++++++++++----- apps/wizard/mainwizard.hpp | 7 ++++- 6 files changed, 65 insertions(+), 33 deletions(-) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index 19a07438db..ac084616c5 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -74,7 +74,7 @@ void Wizard::ComponentSelectionPage::initializePage() componentsList->addItem(bloodmoonItem); } else { - if (mWizard->mInstallations[path]->hasMorrowind) { + if (mWizard->mInstallations[path].hasMorrowind) { morrowindItem->setText(tr("Morrowind\t\t(installed)")); morrowindItem->setFlags(morrowindItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); morrowindItem->setData(Qt::CheckStateRole, Qt::Unchecked); @@ -85,7 +85,7 @@ void Wizard::ComponentSelectionPage::initializePage() componentsList->addItem(morrowindItem); - if (mWizard->mInstallations[path]->hasTribunal) { + if (mWizard->mInstallations[path].hasTribunal) { tribunalItem->setText(tr("Tribunal\t\t(installed)")); tribunalItem->setFlags(tribunalItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); tribunalItem->setData(Qt::CheckStateRole, Qt::Unchecked); @@ -96,7 +96,7 @@ void Wizard::ComponentSelectionPage::initializePage() componentsList->addItem(tribunalItem); - if (mWizard->mInstallations[path]->hasBloodmoon) { + if (mWizard->mInstallations[path].hasBloodmoon) { bloodmoonItem->setText(tr("Bloodmoon\t\t(installed)")); bloodmoonItem->setFlags(bloodmoonItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); bloodmoonItem->setData(Qt::CheckStateRole, Qt::Unchecked); @@ -114,12 +114,12 @@ bool Wizard::ComponentSelectionPage::validatePage() QStringList components(field(QLatin1String("installation.components")).toStringList()); QString path(field(QLatin1String("installation.path")).toString()); - qDebug() << components << path << mWizard->mInstallations[path]; +// qDebug() << components << path << mWizard->mInstallations[path]; if (field(QLatin1String("installation.new")).toBool() == false) { if (components.contains(QLatin1String("Tribunal")) && !components.contains(QLatin1String("Bloodmoon"))) { - if (mWizard->mInstallations[path]->hasBloodmoon) + if (mWizard->mInstallations[path].hasBloodmoon) { QMessageBox msgBox; msgBox.setWindowTitle(tr("About to install Tribunal after Bloodmoon")); @@ -136,7 +136,7 @@ bool Wizard::ComponentSelectionPage::validatePage() if (msgBox.clickedButton() == reinstallButton) { // Force reinstallation - mWizard->mInstallations[path]->hasBloodmoon = false; + mWizard->mInstallations[path].hasBloodmoon = false; QList items = componentsList->findItems(QLatin1String("Bloodmoon"), Qt::MatchStartsWith); foreach (QListWidgetItem *item, items) { diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 66edc1e8a6..0cbdce6731 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -47,7 +47,7 @@ bool Wizard::ExistingInstallationPage::validatePage() // Or failed to be detected due to the target being a symlink QString path(field(QLatin1String("installation.path")).toString()); - QFile file(mWizard->mInstallations[path]->iniPath); + QFile file(mWizard->mInstallations[path].iniPath); if (!file.exists()) { QMessageBox msgBox; @@ -78,7 +78,7 @@ bool Wizard::ExistingInstallationPage::validatePage() // A proper Morrowind.ini was selected, set it QFileInfo info(iniFile); - mWizard->mInstallations[path]->iniPath = info.absoluteFilePath(); + mWizard->mInstallations[path].iniPath = info.absoluteFilePath(); } return true; diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index a0d3dfd21b..f59b6d6408 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -36,11 +36,11 @@ void Wizard::InstallationPage::initializePage() installProgressBar->setMaximum((components.count() * 100)); } else { if (components.contains(QLatin1String("Tribunal")) - && !mWizard->mInstallations[path]->hasTribunal) + && !mWizard->mInstallations[path].hasTribunal) installProgressBar->setMaximum(100); if (components.contains(QLatin1String("Bloodmoon")) - && !mWizard->mInstallations[path]->hasBloodmoon) + && !mWizard->mInstallations[path].hasBloodmoon) installProgressBar->setMaximum(installProgressBar->maximum() + 100); } @@ -104,15 +104,15 @@ void Wizard::InstallationPage::startInstallation() mUnshield->setInstallComponent(Wizard::Component_Morrowind, false); if (components.contains(QLatin1String("Tribunal")) - && !mWizard->mInstallations[path]->hasTribunal) + && !mWizard->mInstallations[path].hasTribunal) mUnshield->setInstallComponent(Wizard::Component_Tribunal, true); if (components.contains(QLatin1String("Bloodmoon")) - && !mWizard->mInstallations[path]->hasBloodmoon) + && !mWizard->mInstallations[path].hasBloodmoon) mUnshield->setInstallComponent(Wizard::Component_Bloodmoon, true); // Set the location of the Morrowind.ini to update - mUnshield->setIniPath(mWizard->mInstallations[path]->iniPath); + mUnshield->setIniPath(mWizard->mInstallations[path].iniPath); mUnshield->setupSettings(); } diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp index 7f97e69183..fbd08ffcd4 100644 --- a/apps/wizard/languageselectionpage.cpp +++ b/apps/wizard/languageselectionpage.cpp @@ -2,6 +2,8 @@ #include "mainwizard.hpp" +#include + Wizard::LanguageSelectionPage::LanguageSelectionPage(MainWizard *wizard) : QWizardPage(wizard), mWizard(wizard) @@ -27,18 +29,22 @@ void Wizard::LanguageSelectionPage::initializePage() int Wizard::LanguageSelectionPage::nextId() const { - // Check if we have to install something - QString path(field(QLatin1String("installation.path")).toString()); - - if (path.isEmpty()) + if (field(QLatin1String("installation.new")).toBool() == true) { return MainWizard::Page_ComponentSelection; - - if (mWizard->mInstallations[path]->hasMorrowind == true && - mWizard->mInstallations[path]->hasTribunal == true && - mWizard->mInstallations[path]->hasBloodmoon == true) - { - return MainWizard::Page_Import; } else { - return MainWizard::Page_ComponentSelection; + QString path(field(QLatin1String("installation.path")).toString()); + + if (path.isEmpty()) + return MainWizard::Page_ComponentSelection; + + // Check if we have to install something + if (mWizard->mInstallations[path].hasMorrowind == true && + mWizard->mInstallations[path].hasTribunal == true && + mWizard->mInstallations[path].hasBloodmoon == true) + { + return MainWizard::Page_Import; + } else { + return MainWizard::Page_ComponentSelection; + } } } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index fa27b548c4..2a4d9f70c1 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -246,23 +246,29 @@ void Wizard::MainWizard::runSettingsImporter() arguments.append(QLatin1String("--cfg")); arguments.append(userPath + QLatin1String("openmw.cfg")); - ProcessInvoker invoker(this); + ProcessInvoker *invoker = new ProcessInvoker(this); - if (!invoker.startProcess(QLatin1String("mwiniimport"), arguments, false)) + if (!invoker->startProcess(QLatin1String("mwiniimport"), arguments, false)) return qApp->quit();; + connect(invoker->getProcess(), SIGNAL(started()), + this, SLOT(importerStarted())); + + connect(invoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(importerFinished(int,QProcess::ExitStatus))); + // Re-read the game settings - setupGameSettings(); + // setupGameSettings(); } void Wizard::MainWizard::addInstallation(const QString &path) { qDebug() << "add installation in: " << path; - Installation* install = new Installation(); + Installation install;// = new Installation(); - install->hasMorrowind = findFiles(QLatin1String("Morrowind"), path); - install->hasTribunal = findFiles(QLatin1String("Tribunal"), path); - install->hasBloodmoon = findFiles(QLatin1String("Bloodmoon"), path); + install.hasMorrowind = findFiles(QLatin1String("Morrowind"), path); + install.hasTribunal = findFiles(QLatin1String("Tribunal"), path); + install.hasBloodmoon = findFiles(QLatin1String("Bloodmoon"), path); // Try to autodetect the Morrowind.ini location QDir dir(path); @@ -276,7 +282,7 @@ void Wizard::MainWizard::addInstallation(const QString &path) } if (file.exists()) - install->iniPath = file.fileName(); + install.iniPath = file.fileName(); mInstallations.insert(QDir::toNativeSeparators(path), install); @@ -301,6 +307,21 @@ void Wizard::MainWizard::setupPages() setStartId(Page_Intro); } +void Wizard::MainWizard::importerStarted() +{ + qDebug() << "importer started!"; +} + +void Wizard::MainWizard::importerFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + qDebug() << "importer finished!"; + if (exitCode != 0 || exitStatus == QProcess::CrashExit) + return; + + // Re-read the settings + setupGameSettings(); +} + void Wizard::MainWizard::accept() { writeSettings(); diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index 63b7e62e8b..ed1a38879b 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -1,6 +1,7 @@ #ifndef MAINWIZARD_HPP #define MAINWIZARD_HPP +#include #include #include @@ -41,7 +42,7 @@ namespace Wizard void addInstallation(const QString &path); void runSettingsImporter(); - QMap mInstallations; + QMap mInstallations; Files::ConfigurationManager mCfgMgr; @@ -66,6 +67,10 @@ namespace Wizard QTextStream *mLog; private slots: + + void importerStarted(); + void importerFinished(int exitCode, QProcess::ExitStatus exitStatus); + void accept(); void reject(); From af883991e2d3410ec67be345c0065f3ee332dcb1 Mon Sep 17 00:00:00 2001 From: pvdk Date: Thu, 17 Apr 2014 02:17:18 +0200 Subject: [PATCH 054/303] Reimplemented the old new profile dialog behaviour --- apps/launcher/datafilespage.cpp | 26 +++++++++++++++++++++----- apps/launcher/datafilespage.hpp | 4 +++- apps/launcher/settingspage.cpp | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 21652a8f40..6b53d89a44 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -30,6 +30,11 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: setObjectName ("DataFilesPage"); mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); + mProfileDialog = new TextInputDialog(tr("New Profile"), tr("Profile name:"), this); + + connect(mProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), + this, SLOT(updateOkButton(QString))); + buildView(); setupDataFiles(); } @@ -212,15 +217,13 @@ void Launcher::DataFilesPage::slotProfileChanged(int index) void Launcher::DataFilesPage::on_newProfileAction_triggered() { - TextInputDialog newDialog (tr("New Profile"), tr("Profile name:"), this); - - if (newDialog.exec() != QDialog::Accepted) + if (!mProfileDialog->exec() == QDialog::Accepted) return; - QString profile = newDialog.lineEdit()->text(); + QString profile = mProfileDialog->lineEdit()->text(); if (profile.isEmpty()) - return; + return; saveSettings(); @@ -271,6 +274,19 @@ void Launcher::DataFilesPage::on_deleteProfileAction_triggered() checkForDefaultProfile(); } +void Launcher::DataFilesPage::updateOkButton(const QString &text) +{ + // We do this here because we need the profiles combobox text + if (text.isEmpty()) { + mProfileDialog->setOkButtonEnabled(false); + return; + } + + (ui.profilesComboBox->findText(text) == -1) + ? mProfileDialog->setOkButtonEnabled(true) + : mProfileDialog->setOkButtonEnabled(false); +} + void Launcher::DataFilesPage::checkForDefaultProfile() { //don't allow deleting "Default" profile diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index 06efdfca3b..908394ae15 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -55,12 +55,14 @@ namespace Launcher void slotProfileRenamed(const QString &previous, const QString ¤t); void slotProfileDeleted(const QString &item); + void updateOkButton(const QString &text); + void on_newProfileAction_triggered(); void on_deleteProfileAction_triggered(); private: - QMenu *mContextMenu; + TextInputDialog *mProfileDialog; Files::ConfigurationManager &mCfgMgr; diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index f673917fdb..8eaec95db8 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -222,7 +222,7 @@ void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus void Launcher::SettingsPage::updateOkButton(const QString &text) { - // We do this here because we need the profiles combobox text + // We do this here because we need to access the profiles if (text.isEmpty()) { mProfileDialog->setOkButtonEnabled(false); return; From aa07a33906bf4b75c27dba1394b7c3d6b3870173 Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 18 Apr 2014 13:17:37 +0200 Subject: [PATCH 055/303] Fixed most of the memory leaks and started working on correctly exiting the thread --- apps/launcher/maindialog.cpp | 49 +++++++------ apps/launcher/maindialog.hpp | 6 ++ apps/launcher/settingspage.cpp | 10 ++- apps/launcher/settingspage.hpp | 1 + apps/wizard/componentselectionpage.cpp | 7 +- apps/wizard/componentselectionpage.hpp | 2 +- apps/wizard/conclusionpage.cpp | 7 +- apps/wizard/conclusionpage.hpp | 2 +- apps/wizard/existinginstallationpage.cpp | 17 ++++- apps/wizard/existinginstallationpage.hpp | 5 +- apps/wizard/importpage.cpp | 7 +- apps/wizard/importpage.hpp | 2 +- apps/wizard/installationpage.cpp | 90 ++++++++++++++---------- apps/wizard/installationpage.hpp | 3 +- apps/wizard/installationtargetpage.cpp | 7 +- apps/wizard/installationtargetpage.hpp | 2 +- apps/wizard/intropage.cpp | 7 +- apps/wizard/intropage.hpp | 2 +- apps/wizard/languageselectionpage.cpp | 7 +- apps/wizard/languageselectionpage.hpp | 2 +- apps/wizard/mainwizard.cpp | 80 +++++++++++++-------- apps/wizard/mainwizard.hpp | 7 +- apps/wizard/methodselectionpage.cpp | 7 +- apps/wizard/methodselectionpage.hpp | 2 +- apps/wizard/unshield/unshieldworker.cpp | 23 +++++- apps/wizard/unshield/unshieldworker.hpp | 4 ++ components/process/processinvoker.cpp | 16 ++--- components/process/processinvoker.hpp | 2 +- files/ui/wizard/importpage.ui | 7 +- 29 files changed, 246 insertions(+), 137 deletions(-) diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 9c6e33e1b1..918cd3d415 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -1,7 +1,6 @@ #include "maindialog.hpp" #include -#include #include #include @@ -35,27 +34,10 @@ using namespace Process; Launcher::MainDialog::MainDialog(QWidget *parent) : mGameSettings(mCfgMgr), QMainWindow (parent) { - // Install the stylesheet font - QFile file; - QFontDatabase fontDatabase; - - const QStringList fonts = fontDatabase.families(); - - // Check if the font is installed - if (!fonts.contains("EB Garamond")) { - - QString font = QString::fromStdString(mCfgMgr.getGlobalDataPath().string()) + QString("resources/mygui/EBGaramond-Regular.ttf"); - file.setFileName(font); - - if (!file.exists()) { - font = QString::fromStdString(mCfgMgr.getLocalPath().string()) + QString("resources/mygui/EBGaramond-Regular.ttf"); - } - - fontDatabase.addApplicationFont(font); - } - setupUi(this); + mGameInvoker = new ProcessInvoker(); + iconWidget->setViewMode(QListView::IconMode); iconWidget->setWrapping(false); iconWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Just to be sure @@ -94,9 +76,33 @@ Launcher::MainDialog::MainDialog(QWidget *parent) QLatin1String("hh:mm:ss")).toString(Qt::SystemLocaleShortDate))); } + // Install the stylesheet font + QFile file; + QFontDatabase fontDatabase; + + const QStringList fonts = fontDatabase.families(); + + // Check if the font is installed + if (!fonts.contains("EB Garamond")) { + + QString font = QString::fromStdString(mCfgMgr.getGlobalDataPath().string()) + QString("resources/mygui/EBGaramond-Regular.ttf"); + file.setFileName(font); + + if (!file.exists()) { + font = QString::fromStdString(mCfgMgr.getLocalPath().string()) + QString("resources/mygui/EBGaramond-Regular.ttf"); + } + + fontDatabase.addApplicationFont(font); + } + createIcons(); } +Launcher::MainDialog::~MainDialog() +{ + delete mGameInvoker; +} + void Launcher::MainDialog::createIcons() { if (!QIcon::hasThemeIcon("document-new")) @@ -801,8 +807,7 @@ void Launcher::MainDialog::play() } // Launch the game detached - ProcessInvoker invoker(this); - if (invoker.startProcess(QLatin1String("openmw"), true)) + if (mGameInvoker->startProcess(QLatin1String("openmw"), true)) qApp->quit(); } diff --git a/apps/launcher/maindialog.hpp b/apps/launcher/maindialog.hpp index 7f468de9c8..fefbaecaf9 100644 --- a/apps/launcher/maindialog.hpp +++ b/apps/launcher/maindialog.hpp @@ -5,6 +5,9 @@ #ifndef Q_MOC_RUN #include #endif + +#include + #include #include @@ -36,6 +39,8 @@ namespace Launcher public: explicit MainDialog(QWidget *parent = 0); + ~MainDialog(); + bool setup(); bool showFirstRunDialog(); @@ -67,6 +72,7 @@ namespace Launcher DataFilesPage *mDataFilesPage; SettingsPage *mSettingsPage; + Process::ProcessInvoker *mGameInvoker; Files::ConfigurationManager mCfgMgr; diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 8eaec95db8..fe9369769b 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -36,8 +36,8 @@ Launcher::SettingsPage::SettingsPage(Files::ConfigurationManager &cfg, languageComboBox->addItems(languages); - mWizardInvoker = new ProcessInvoker(this); - mImporterInvoker = new ProcessInvoker(this); + mWizardInvoker = new ProcessInvoker(); + mImporterInvoker = new ProcessInvoker(); connect(mWizardInvoker->getProcess(), SIGNAL(started()), this, SLOT(wizardStarted())); @@ -85,6 +85,12 @@ Launcher::SettingsPage::SettingsPage(Files::ConfigurationManager &cfg, loadSettings(); } +Launcher::SettingsPage::~SettingsPage() +{ + delete mWizardInvoker; + delete mImporterInvoker; +} + void Launcher::SettingsPage::on_wizardButton_clicked() { saveSettings(); diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index 846f5681c8..124c806009 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -25,6 +25,7 @@ namespace Launcher public: SettingsPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, Config::LauncherSettings &launcherSettings, MainDialog *parent = 0); + ~SettingsPage(); void saveSettings(); bool loadSettings(); diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index ac084616c5..d372f677d0 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -7,10 +7,11 @@ #include "mainwizard.hpp" -Wizard::ComponentSelectionPage::ComponentSelectionPage(MainWizard *wizard) : - QWizardPage(wizard), - mWizard(wizard) +Wizard::ComponentSelectionPage::ComponentSelectionPage(QWidget *parent) : + QWizardPage(parent) { + mWizard = qobject_cast(parent); + setupUi(this); setCommitPage(true); diff --git a/apps/wizard/componentselectionpage.hpp b/apps/wizard/componentselectionpage.hpp index ed007fd087..ca43471084 100644 --- a/apps/wizard/componentselectionpage.hpp +++ b/apps/wizard/componentselectionpage.hpp @@ -13,7 +13,7 @@ namespace Wizard { Q_OBJECT public: - ComponentSelectionPage(MainWizard *wizard); + ComponentSelectionPage(QWidget *parent); int nextId() const; virtual bool validatePage(); diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index 4407ac5475..87154732ae 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -4,10 +4,11 @@ #include "mainwizard.hpp" -Wizard::ConclusionPage::ConclusionPage(MainWizard *wizard) : - QWizardPage(wizard), - mWizard(wizard) +Wizard::ConclusionPage::ConclusionPage(QWidget *parent) : + QWizardPage(parent) { + mWizard = qobject_cast(parent); + setupUi(this); setPixmap(QWizard::WatermarkPixmap, QPixmap(QLatin1String(":/images/intropage-background.png"))); } diff --git a/apps/wizard/conclusionpage.hpp b/apps/wizard/conclusionpage.hpp index e050684e15..0e9abed72c 100644 --- a/apps/wizard/conclusionpage.hpp +++ b/apps/wizard/conclusionpage.hpp @@ -13,7 +13,7 @@ namespace Wizard { Q_OBJECT public: - ConclusionPage(MainWizard *wizard); + ConclusionPage(QWidget *parent); int nextId() const; diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 0cbdce6731..7c5d10a802 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -8,16 +8,27 @@ #include "mainwizard.hpp" -Wizard::ExistingInstallationPage::ExistingInstallationPage(MainWizard *wizard) : - QWizardPage(wizard), - mWizard(wizard) +Wizard::ExistingInstallationPage::ExistingInstallationPage(QWidget *parent) : + QWizardPage(parent) { + mWizard = qobject_cast(parent); + setupUi(this); +} + +void Wizard::ExistingInstallationPage::initializePage() +{ QListWidgetItem *emptyItem = new QListWidgetItem(tr("No existing installations detected")); emptyItem->setFlags(Qt::NoItemFlags); installationsList->addItem(emptyItem); + // Test + if (mWizard->mInstallations.isEmpty()) { + qDebug() << "crashy crash"; + return; + } + // Add the available installation paths QStringList paths(mWizard->mInstallations.keys()); diff --git a/apps/wizard/existinginstallationpage.hpp b/apps/wizard/existinginstallationpage.hpp index 54ee8d2e2f..6012954642 100644 --- a/apps/wizard/existinginstallationpage.hpp +++ b/apps/wizard/existinginstallationpage.hpp @@ -13,7 +13,7 @@ namespace Wizard { Q_OBJECT public: - ExistingInstallationPage(MainWizard *wizard); + ExistingInstallationPage(QWidget *parent); int nextId() const; virtual bool isComplete() const; @@ -27,6 +27,9 @@ namespace Wizard private: MainWizard *mWizard; + protected: + void initializePage(); + }; } diff --git a/apps/wizard/importpage.cpp b/apps/wizard/importpage.cpp index b49105faa9..71c5544e6b 100644 --- a/apps/wizard/importpage.cpp +++ b/apps/wizard/importpage.cpp @@ -2,10 +2,11 @@ #include "mainwizard.hpp" -Wizard::ImportPage::ImportPage(MainWizard *wizard) : - QWizardPage(wizard), - mWizard(wizard) +Wizard::ImportPage::ImportPage(QWidget *parent) : + QWizardPage(parent) { + mWizard = qobject_cast(parent); + setupUi(this); registerField(QLatin1String("installation.import-settings"), importCheckBox); diff --git a/apps/wizard/importpage.hpp b/apps/wizard/importpage.hpp index 2eae08531c..386cd59af0 100644 --- a/apps/wizard/importpage.hpp +++ b/apps/wizard/importpage.hpp @@ -13,7 +13,7 @@ namespace Wizard { Q_OBJECT public: - ImportPage(MainWizard *wizard); + ImportPage(QWidget *parent); int nextId() const; diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index f59b6d6408..b76aeea228 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -9,48 +9,14 @@ #include "mainwizard.hpp" #include "inisettings.hpp" -Wizard::InstallationPage::InstallationPage(MainWizard *wizard) : - QWizardPage(wizard), - mWizard(wizard) +Wizard::InstallationPage::InstallationPage(QWidget *parent) : + QWizardPage(parent) { + mWizard = qobject_cast(parent); + setupUi(this); mFinished = false; -} - -void Wizard::InstallationPage::initializePage() -{ - QString path(field(QLatin1String("installation.path")).toString()); - QStringList components(field(QLatin1String("installation.components")).toStringList()); - - logTextEdit->appendPlainText(QString("Installing to %1").arg(path)); - logTextEdit->appendPlainText(QString("Installing %1.").arg(components.join(", "))); - - installProgressBar->setMinimum(0); - - // Set the progressbar maximum to a multiple of 100 - // That way installing all three components would yield 300% - // When one component is done the bar will be filled by 33% - - if (field(QLatin1String("installation.new")).toBool() == true) { - installProgressBar->setMaximum((components.count() * 100)); - } else { - if (components.contains(QLatin1String("Tribunal")) - && !mWizard->mInstallations[path].hasTribunal) - installProgressBar->setMaximum(100); - - if (components.contains(QLatin1String("Bloodmoon")) - && !mWizard->mInstallations[path].hasBloodmoon) - installProgressBar->setMaximum(installProgressBar->maximum() + 100); - } - - startInstallation(); -} - -void Wizard::InstallationPage::startInstallation() -{ - QStringList components(field(QLatin1String("installation.components")).toStringList()); - QString path(field(QLatin1String("installation.path")).toString()); mThread = new QThread(); mUnshield = new UnshieldWorker(); @@ -88,6 +54,54 @@ void Wizard::InstallationPage::startInstallation() connect(mUnshield, SIGNAL(requestFileDialog(Wizard::Component)), this, SLOT(showFileDialog(Wizard::Component)), Qt::QueuedConnection); +} + +Wizard::InstallationPage::~InstallationPage() +{ + qDebug() << "stop!"; + + if (mThread->isRunning()) { + mUnshield->stopWorker(); + mThread->wait(); + } + + delete mUnshield; + delete mThread; +} + +void Wizard::InstallationPage::initializePage() +{ + QString path(field(QLatin1String("installation.path")).toString()); + QStringList components(field(QLatin1String("installation.components")).toStringList()); + + logTextEdit->appendPlainText(QString("Installing to %1").arg(path)); + logTextEdit->appendPlainText(QString("Installing %1.").arg(components.join(", "))); + + installProgressBar->setMinimum(0); + + // Set the progressbar maximum to a multiple of 100 + // That way installing all three components would yield 300% + // When one component is done the bar will be filled by 33% + + if (field(QLatin1String("installation.new")).toBool() == true) { + installProgressBar->setMaximum((components.count() * 100)); + } else { + if (components.contains(QLatin1String("Tribunal")) + && !mWizard->mInstallations[path].hasTribunal) + installProgressBar->setMaximum(100); + + if (components.contains(QLatin1String("Bloodmoon")) + && !mWizard->mInstallations[path].hasBloodmoon) + installProgressBar->setMaximum(installProgressBar->maximum() + 100); + } + + startInstallation(); +} + +void Wizard::InstallationPage::startInstallation() +{ + QStringList components(field(QLatin1String("installation.components")).toStringList()); + QString path(field(QLatin1String("installation.path")).toString()); if (field(QLatin1String("installation.new")).toBool() == true) { diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index 5e58477bed..822cd21cd8 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -19,7 +19,8 @@ namespace Wizard { Q_OBJECT public: - InstallationPage(MainWizard *wizard); + InstallationPage(QWidget *parent); + ~InstallationPage(); int nextId() const; virtual bool isComplete() const; diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index d89d54a00b..3a179a2cbe 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -6,11 +6,12 @@ #include "mainwizard.hpp" -Wizard::InstallationTargetPage::InstallationTargetPage(MainWizard *wizard, const Files::ConfigurationManager &cfg) : - QWizardPage(wizard), - mWizard(wizard), +Wizard::InstallationTargetPage::InstallationTargetPage(QWidget *parent, const Files::ConfigurationManager &cfg) : + QWizardPage(parent), mCfgMgr(cfg) { + mWizard = qobject_cast(parent); + setupUi(this); registerField(QLatin1String("installation.path*"), targetLineEdit); diff --git a/apps/wizard/installationtargetpage.hpp b/apps/wizard/installationtargetpage.hpp index 0711c1c050..ca3b505b7e 100644 --- a/apps/wizard/installationtargetpage.hpp +++ b/apps/wizard/installationtargetpage.hpp @@ -18,7 +18,7 @@ namespace Wizard { Q_OBJECT public: - InstallationTargetPage(MainWizard *wizard, const Files::ConfigurationManager &cfg); + InstallationTargetPage(QWidget *parent, const Files::ConfigurationManager &cfg); int nextId() const; virtual bool validatePage(); diff --git a/apps/wizard/intropage.cpp b/apps/wizard/intropage.cpp index 91e7d5dc07..0a98ae5f3f 100644 --- a/apps/wizard/intropage.cpp +++ b/apps/wizard/intropage.cpp @@ -2,10 +2,11 @@ #include "mainwizard.hpp" -Wizard::IntroPage::IntroPage(MainWizard *wizard) : - QWizardPage(wizard), - mWizard(wizard) +Wizard::IntroPage::IntroPage(QWidget *parent) : + QWizardPage(parent) { + mWizard = qobject_cast(parent); + setupUi(this); setPixmap(QWizard::WatermarkPixmap, QPixmap(QLatin1String(":/images/intropage-background.png"))); } diff --git a/apps/wizard/intropage.hpp b/apps/wizard/intropage.hpp index 78bac3155e..4ad9b4111b 100644 --- a/apps/wizard/intropage.hpp +++ b/apps/wizard/intropage.hpp @@ -13,7 +13,7 @@ namespace Wizard { Q_OBJECT public: - IntroPage(MainWizard *wizard); + IntroPage(QWidget *parent); int nextId() const; diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp index fbd08ffcd4..0d5132f5bf 100644 --- a/apps/wizard/languageselectionpage.cpp +++ b/apps/wizard/languageselectionpage.cpp @@ -4,10 +4,11 @@ #include -Wizard::LanguageSelectionPage::LanguageSelectionPage(MainWizard *wizard) : - QWizardPage(wizard), - mWizard(wizard) +Wizard::LanguageSelectionPage::LanguageSelectionPage(QWidget *parent) : + QWizardPage(parent) { + mWizard = qobject_cast(parent); + setupUi(this); registerField(QLatin1String("installation.language"), languageComboBox); diff --git a/apps/wizard/languageselectionpage.hpp b/apps/wizard/languageselectionpage.hpp index 3c17514f98..abc3edb98a 100644 --- a/apps/wizard/languageselectionpage.hpp +++ b/apps/wizard/languageselectionpage.hpp @@ -13,7 +13,7 @@ namespace Wizard { Q_OBJECT public: - LanguageSelectionPage(MainWizard *wizard); + LanguageSelectionPage(QWidget *parent); int nextId() const; diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 2a4d9f70c1..92743417e0 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -1,7 +1,5 @@ #include "mainwizard.hpp" -#include - #include #include @@ -25,7 +23,9 @@ using namespace Process; Wizard::MainWizard::MainWizard(QWidget *parent) : mGameSettings(mCfgMgr), - QWizard(parent) + QWizard(parent), + mError(false), + mInstallations() { #ifndef Q_OS_MAC setWizardStyle(QWizard::ModernStyle); @@ -37,11 +37,26 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setWindowIcon(QIcon(QLatin1String(":/images/openmw-wizard.png"))); setMinimumWidth(550); + // This prevents initializePage() being called multiple times + setOption(QWizard::IndependentPages); + // Set the property for comboboxes to the text instead of index setDefaultProperty("QComboBox", "currentText", "currentIndexChanged"); setDefaultProperty("ComponentListWidget", "mCheckedItems", "checkedItemsChanged"); + mImporterInvoker = new ProcessInvoker(); + + connect(mImporterInvoker->getProcess(), SIGNAL(started()), + this, SLOT(importerStarted())); + + connect(mImporterInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(importerFinished(int,QProcess::ExitStatus))); + + mLogError = tr("

Could not open %1 for writing

\ +

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

"); + setupLog(); setupGameSettings(); setupLauncherSettings(); @@ -49,44 +64,58 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setupPages(); } +Wizard::MainWizard::~MainWizard() +{ + delete mImporterInvoker; +} + void Wizard::MainWizard::setupLog() { QString logPath(QString::fromUtf8(mCfgMgr.getLogPath().string().c_str())); logPath.append(QLatin1String("wizard.log")); - QString message(tr("

Could not open %1 for writing

\ -

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

")); + QFile file(logPath); - QFile *file = new QFile(logPath); - - if (!file->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) { + if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { QMessageBox msgBox; msgBox.setWindowTitle(tr("Error opening Wizard log file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(message.arg(file->fileName())); + msgBox.setText(mLogError.arg(file.fileName())); msgBox.exec(); return qApp->quit(); } + addLogText(QString("Started OpenMW Wizard on %1").arg(QDateTime::currentDateTime().toString())); + qDebug() << logPath; - - mLog = new QTextStream(file); - mLog->setCodec(QTextCodec::codecForName("UTF-8")); - - //addLogText(QLatin1String("test test 123 test")); } void Wizard::MainWizard::addLogText(const QString &text) { + QString logPath(QString::fromUtf8(mCfgMgr.getLogPath().string().c_str())); + logPath.append(QLatin1String("wizard.log")); - qDebug() << "AddLogText called! " << text; - if (!text.isEmpty()) { - qDebug() << "logging " << text; - *mLog << text << endl; + QFile file(logPath); + + if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening Wizard log file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(mLogError.arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); } + if (!file.isSequential()) + file.seek(file.size()); + + QTextStream out(&file); + + if (!text.isEmpty()) + out << text << endl; + // file.close(); } @@ -246,16 +275,8 @@ void Wizard::MainWizard::runSettingsImporter() arguments.append(QLatin1String("--cfg")); arguments.append(userPath + QLatin1String("openmw.cfg")); - ProcessInvoker *invoker = new ProcessInvoker(this); - - if (!invoker->startProcess(QLatin1String("mwiniimport"), arguments, false)) - return qApp->quit();; - - connect(invoker->getProcess(), SIGNAL(started()), - this, SLOT(importerStarted())); - - connect(invoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), - this, SLOT(importerFinished(int,QProcess::ExitStatus))); + if (!mImporterInvoker->startProcess(QLatin1String("mwiniimport"), arguments, false)) + return qApp->quit(); // Re-read the game settings // setupGameSettings(); @@ -305,6 +326,7 @@ void Wizard::MainWizard::setupPages() setPage(Page_Import, new ImportPage(this)); setPage(Page_Conclusion, new ConclusionPage(this)); setStartId(Page_Intro); + } void Wizard::MainWizard::importerStarted() diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index ed1a38879b..6b8c4931fe 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -5,6 +5,8 @@ #include #include +#include + #include #include #include @@ -37,6 +39,7 @@ namespace Wizard }; MainWizard(QWidget *parent = 0); + ~MainWizard(); bool findFiles(const QString &name, const QString &path); void addInstallation(const QString &path); @@ -46,6 +49,8 @@ namespace Wizard Files::ConfigurationManager mCfgMgr; + Process::ProcessInvoker *mImporterInvoker; + bool mError; public slots: @@ -64,7 +69,7 @@ namespace Wizard Config::GameSettings mGameSettings; Config::LauncherSettings mLauncherSettings; - QTextStream *mLog; + QString mLogError; private slots: diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp index b1483ac3a5..d7c64f3b03 100644 --- a/apps/wizard/methodselectionpage.cpp +++ b/apps/wizard/methodselectionpage.cpp @@ -2,10 +2,11 @@ #include #include "mainwizard.hpp" -Wizard::MethodSelectionPage::MethodSelectionPage(MainWizard *wizard) : - QWizardPage(wizard), - mWizard(wizard) +Wizard::MethodSelectionPage::MethodSelectionPage(QWidget *parent) : + QWizardPage(parent) { + mWizard = qobject_cast(parent); + setupUi(this); registerField(QLatin1String("installation.new"), newLocationRadioButton); diff --git a/apps/wizard/methodselectionpage.hpp b/apps/wizard/methodselectionpage.hpp index de34ff555d..60941c651d 100644 --- a/apps/wizard/methodselectionpage.hpp +++ b/apps/wizard/methodselectionpage.hpp @@ -13,7 +13,7 @@ namespace Wizard { Q_OBJECT public: - MethodSelectionPage(MainWizard *wizard); + MethodSelectionPage(QWidget *parent); int nextId() const; diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 81282f5477..5fa0433476 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -35,6 +35,8 @@ Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : mTribunalDone = false; mBloodmoonDone = false; + mStopped = false; + qRegisterMetaType("Wizard::Component"); } @@ -42,6 +44,13 @@ Wizard::UnshieldWorker::~UnshieldWorker() { } +void Wizard::UnshieldWorker::stopWorker() +{ + mMutex.lock(); + mStopped = true; + mMutex.unlock(); +} + void Wizard::UnshieldWorker::setInstallComponent(Wizard::Component component, bool install) { QWriteLocker writeLock(&mLock); @@ -781,6 +790,13 @@ bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &d for (size_t j=group->first_file; j<=group->last_file; ++j) { + if (mStopped) { + qDebug() << "We're asked to stop!"; + + unshield_close(unshield); + return true; + } + if (unshield_file_is_valid(unshield, j)) { success = extractFile(unshield, destination, group->name, j, counter); @@ -789,6 +805,8 @@ bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &d emit error(tr("Failed to extract %1.").arg(name), tr("Complete path: %1").arg(destination + QDir::separator() + name)); + + unshield_close(unshield); return false; } @@ -892,11 +910,14 @@ bool Wizard::UnshieldWorker::findInCab(const QString &fileName, const QString &c if (unshield_file_is_valid(unshield, j)) { QString current(QString::fromUtf8(unshield_file_name(unshield, j))); - if (current.toLower() == fileName.toLower()) + if (current.toLower() == fileName.toLower()) { + unshield_close(unshield); return true; // File is found! + } } } } + unshield_close(unshield); return false; } diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index 2e509d1168..5ea7b04aee 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -30,6 +30,8 @@ namespace Wizard UnshieldWorker(QObject *parent = 0); ~UnshieldWorker(); + void stopWorker(); + void setInstallComponent(Wizard::Component component, bool install); void setDiskPath(const QString &path); @@ -92,6 +94,8 @@ namespace Wizard bool mTribunalDone; bool mBloodmoonDone; + bool mStopped; + QString mPath; QString mIniPath; QString mDiskPath; diff --git a/components/process/processinvoker.cpp b/components/process/processinvoker.cpp index 7ac8d3c93a..44a11cc4e2 100644 --- a/components/process/processinvoker.cpp +++ b/components/process/processinvoker.cpp @@ -8,10 +8,17 @@ #include #include -Process::ProcessInvoker::ProcessInvoker(QWidget *parent) +Process::ProcessInvoker::ProcessInvoker() { mProcess = new QProcess(this); + connect(mProcess, SIGNAL(error(QProcess::ProcessError)), + this, SLOT(processError(QProcess::ProcessError))); + + connect(mProcess, SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(processFinished(int,QProcess::ExitStatus))); + + mName = QString(); mArguments = QStringList(); } @@ -48,13 +55,6 @@ QProcess* Process::ProcessInvoker::getProcess() bool Process::ProcessInvoker::startProcess(const QString &name, const QStringList &arguments, bool detached) { // mProcess = new QProcess(this); - - connect(mProcess, SIGNAL(error(QProcess::ProcessError)), - this, SLOT(processError(QProcess::ProcessError))); - - connect(mProcess, SIGNAL(finished(int,QProcess::ExitStatus)), - this, SLOT(processFinished(int,QProcess::ExitStatus))); - mName = name; mArguments = arguments; diff --git a/components/process/processinvoker.hpp b/components/process/processinvoker.hpp index 07907e3b4c..8fff6658ca 100644 --- a/components/process/processinvoker.hpp +++ b/components/process/processinvoker.hpp @@ -13,7 +13,7 @@ namespace Process public: - ProcessInvoker(QWidget *parent = 0); + ProcessInvoker(); ~ProcessInvoker(); // void setProcessName(const QString &name); diff --git a/files/ui/wizard/importpage.ui b/files/ui/wizard/importpage.ui index 52f9ac4f2e..9fd4ea5000 100644 --- a/files/ui/wizard/importpage.ui +++ b/files/ui/wizard/importpage.ui @@ -6,8 +6,8 @@ 0 0 - 510 - 324 + 508 + 322
@@ -45,6 +45,9 @@ Import add-on and plugin selection + + true + From 5af12d193acfe5f0470ed947cffff1072b56daf3 Mon Sep 17 00:00:00 2001 From: pvdk Date: Thu, 29 May 2014 17:57:41 +0200 Subject: [PATCH 056/303] Cleaned up old wizard stuff from launcher --- CMakeLists.txt | 16 +- apps/launcher/CMakeLists.txt | 24 +- apps/launcher/main.cpp | 9 +- apps/launcher/maindialog.cpp | 387 +++++---------- apps/launcher/maindialog.hpp | 7 + apps/launcher/settingspage.cpp | 6 +- apps/launcher/unshieldthread.cpp | 521 -------------------- apps/launcher/unshieldthread.hpp | 58 --- apps/launcher/utils/checkablemessagebox.cpp | 258 ---------- apps/launcher/utils/checkablemessagebox.hpp | 116 ----- 10 files changed, 141 insertions(+), 1261 deletions(-) delete mode 100644 apps/launcher/unshieldthread.cpp delete mode 100644 apps/launcher/unshieldthread.hpp delete mode 100644 apps/launcher/utils/checkablemessagebox.cpp delete mode 100644 apps/launcher/utils/checkablemessagebox.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a9f618d17..66746b22c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -406,7 +406,7 @@ IF(NOT WIN32 AND NOT APPLE) IF(BUILD_WIZARD) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/wizard" DESTINATION "${BINDIR}" ) ENDIF(BUILD_WIZARD) - + # Install licenses INSTALL(FILES "DejaVu Font License.txt" DESTINATION "${LICDIR}" ) @@ -460,7 +460,7 @@ if(WIN32) IF(BUILD_WIZARD) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/openmw-wizard.exe" DESTINATION ".") ENDIF(BUILD_WIZARD) - + INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION ".") @@ -543,13 +543,6 @@ if (BUILD_ESMTOOL) endif() if (BUILD_LAUNCHER) - if(NOT WIN32) - find_package(LIBUNSHIELD REQUIRED) - if(NOT LIBUNSHIELD_FOUND) - message(SEND_ERROR "Failed to find libunshield") - endif(NOT LIBUNSHIELD_FOUND) - endif(NOT WIN32) - add_subdirectory( apps/launcher ) endif() @@ -562,6 +555,11 @@ if (BUILD_OPENCS) endif() if (BUILD_WIZARD) + find_package(LIBUNSHIELD REQUIRED) + if(NOT LIBUNSHIELD_FOUND) + message(FATAL_ERROR "Failed to find Unshield library") + endif(NOT LIBUNSHIELD_FOUND) + add_subdirectory(apps/wizard) endif() diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index b777856e81..eb11cab499 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -9,16 +9,12 @@ set(LAUNCHER settings/graphicssettings.cpp - utils/checkablemessagebox.cpp utils/profilescombobox.cpp utils/textinputdialog.cpp utils/lineedit.cpp ${CMAKE_SOURCE_DIR}/files/launcher/launcher.rc ) -if(NOT WIN32) - LIST(APPEND LAUNCHER unshieldthread.cpp) -endif(NOT WIN32) set(LAUNCHER_HEADER datafilespage.hpp @@ -30,15 +26,10 @@ set(LAUNCHER_HEADER settings/graphicssettings.hpp - utils/checkablemessagebox.hpp utils/profilescombobox.hpp utils/textinputdialog.hpp utils/lineedit.hpp ) -if(NOT WIN32) - LIST(APPEND LAUNCHER_HEADER unshieldthread.hpp) -endif(NOT WIN32) - # Headers that must be pre-processed set(LAUNCHER_HEADER_MOC @@ -50,17 +41,11 @@ set(LAUNCHER_HEADER_MOC settingspage.hpp utils/textinputdialog.hpp - utils/checkablemessagebox.hpp utils/profilescombobox.hpp utils/lineedit.hpp ) -if(NOT WIN32) - LIST(APPEND LAUNCHER_HEADER_MOC unshieldthread.hpp) -endif(NOT WIN32) - - set(LAUNCHER_UI ${CMAKE_SOURCE_DIR}/files/ui/datafilespage.ui ${CMAKE_SOURCE_DIR}/files/ui/graphicspage.ui @@ -119,19 +104,12 @@ target_link_libraries(omwlauncher ${QT_LIBRARIES} components ) -if(NOT WIN32) - target_link_libraries(omwlauncher - ${LIBUNSHIELD_LIBRARY} - ) -endif(NOT WIN32) - - if(DPKG_PROGRAM) INSTALL(TARGETS omwlauncher RUNTIME DESTINATION games COMPONENT omwlauncher) endif() -if (BUILD_WITH_CODE_COVERAGE) +if(BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(omwlauncher gcov) endif() diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index fabf77d901..67320bff0f 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -52,11 +52,12 @@ int main(int argc, char *argv[]) Launcher::MainDialog mainWin; - if (mainWin.setup()) { - mainWin.show(); - } else { + if (!mainWin.setup()) { return 0; - } + //mainWin.show(); + }/* else { + return 0; + }*/ int returnValue = app.exec(); SDL_Quit(); diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 3f9f27554f..76b9ad3c54 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -16,14 +17,6 @@ #include -#ifndef WIN32 - #include "unshieldthread.hpp" -#endif - -#include "textslotmsgbox.hpp" - -#include "utils/checkablemessagebox.hpp" - #include "playpage.hpp" #include "graphicspage.hpp" #include "datafilespage.hpp" @@ -56,6 +49,10 @@ Launcher::MainDialog::MainDialog(QWidget *parent) setupUi(this); mGameInvoker = new ProcessInvoker(); + mWizardInvoker = new ProcessInvoker(); + + connect(mWizardInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(wizardFinished(int,QProcess::ExitStatus))); iconWidget->setViewMode(QListView::IconMode); iconWidget->setWrapping(false); @@ -83,13 +80,13 @@ Launcher::MainDialog::MainDialog(QWidget *parent) if (!revision.isEmpty() && !tag.isEmpty()) { if (revision == tag) { - versionLabel->setText(tr("OpenMW %0 release").arg(OPENMW_VERSION)); + versionLabel->setText(tr("OpenMW %1 release").arg(OPENMW_VERSION)); } else { - versionLabel->setText(tr("OpenMW development (%0)").arg(revision.left(10))); + versionLabel->setText(tr("OpenMW development (%1)").arg(revision.left(10))); } // Add the compile date and time - versionLabel->setToolTip(tr("Compiled on %0 %1").arg(QLocale(QLocale::C).toDate(QString(__DATE__).simplified(), + versionLabel->setToolTip(tr("Compiled on %1 %2").arg(QLocale(QLocale::C).toDate(QString(__DATE__).simplified(), QLatin1String("MMM d yyyy")).toString(Qt::SystemLocaleLongDate), QLocale(QLocale::C).toTime(QString(__TIME__).simplified(), QLatin1String("hh:mm:ss")).toString(Qt::SystemLocaleShortDate))); @@ -101,6 +98,7 @@ Launcher::MainDialog::MainDialog(QWidget *parent) Launcher::MainDialog::~MainDialog() { delete mGameInvoker; + delete mWizardInvoker; } void Launcher::MainDialog::createIcons() @@ -167,102 +165,32 @@ void Launcher::MainDialog::createPages() bool Launcher::MainDialog::showFirstRunDialog() { + 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); + Q_UNUSED(skipButton); // Surpress compiler unused warning -// CheckableMessageBox msgBox(this); -// msgBox.setWindowTitle(tr("Morrowind installation detected")); + msgBox.exec(); -// QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion); -// int size = QApplication::style()->pixelMetric(QStyle::PM_MessageBoxIconSize); -// msgBox.setIconPixmap(icon.pixmap(size, size)); - -// QAbstractButton *importerButton = -// msgBox.addButton(tr("Import"), QDialogButtonBox::AcceptRole); // ActionRole doesn't work?! -// QAbstractButton *skipButton = -// msgBox.addButton(tr("Skip"), QDialogButtonBox::RejectRole); - -// Q_UNUSED(skipButton); // Surpress compiler unused warning - -// msgBox.setStandardButtons(QDialogButtonBox::NoButton); -// msgBox.setText(tr("
An existing Morrowind configuration was detected
\ -//
Would you like to import settings from Morrowind.ini?
\ -//
Warning: In most cases OpenMW needs these settings to run properly
")); -// msgBox.setCheckBoxText(tr("Include selected masters and plugins (creates a new profile)")); -// msgBox.exec(); - - -// if (msgBox.clickedButton() == importerButton) { - -// if (iniPaths.count() > 1) { -// // Multiple Morrowind.ini files found -// bool ok; -// QString path = QInputDialog::getItem(this, tr("Multiple configurations found"), -// tr("
There are multiple Morrowind.ini files found.

\ -// Please select the one you wish to import from:"), iniPaths, 0, false, &ok); -// if (ok && !path.isEmpty()) { -// iniPaths.clear(); -// iniPaths.append(path); -// } else { -// // Cancel was clicked -// return false; -// } -// } - -// // Create the file if it doesn't already exist, else the importer will fail -// QString path = QString::fromStdString(mCfgMgr.getUserConfigPath().string()) + QString("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 %0 for writing

\ -// Please make sure you have the right permissions \ -// and try again.
").arg(file.fileName())); -// msgBox.exec(); -// return false; -// } - -// file.close(); -// } - -// // Construct the arguments to run the importer -// QStringList arguments; - -// if (msgBox.isChecked()) -// arguments.append(QString("--game-files")); - -// arguments.append(QString("--encoding")); -// arguments.append(mGameSettings.value(QString("encoding"), QString("win1252"))); -// arguments.append(QString("--ini")); -// arguments.append(iniPaths.first()); -// arguments.append(QString("--cfg")); -// arguments.append(path); - -// ProcessInvoker invoker(this); - -// if (!invoker.startProcess(QLatin1String("mwiniimport"), arguments, false)) -// return false; - -// // Re-read the game settings -// if (!setupGameSettings()) -// return false; - -// // Add a new profile -// if (msgBox.isChecked()) { -// mLauncherSettings.setValue(QString("Profiles/currentprofile"), QString("Imported")); -// mLauncherSettings.remove(QString("Profiles/Imported/content")); - -// QStringList contents = mGameSettings.values(QString("content")); -// foreach (const QString &content, contents) { -// mLauncherSettings.setMultiValue(QString("Profiles/Imported/content"), content); -// } -// } - -// } + if (msgBox.clickedButton() == wizardButton) + { + if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) { + return false; + } else { + return true; + } + } + show(); return true; } @@ -277,13 +205,6 @@ bool Launcher::MainDialog::setup() if (!setupGraphicsSettings()) return false; - // Check if we need to show the importer - if (mLauncherSettings.value(QString("General/firstrun"), QString("true")) == QLatin1String("true")) - { - if (!showFirstRunDialog()) - return false; - } - // Now create the pages as they need the settings createPages(); @@ -292,6 +213,18 @@ bool Launcher::MainDialog::setup() return false; loadSettings(); + + // Check if we need to run the wizard + if (mLauncherSettings.value(QString("General/firstrun"), QString("true")) == QLatin1String("true")) + { + if (!showFirstRunDialog()) { + return false; + } else { + return true; + } + } + + show(); // Show ourselves if the wizard is not being run return true; } @@ -328,20 +261,20 @@ void Launcher::MainDialog::changePage(QListWidgetItem *current, QListWidgetItem pagesWidget->setCurrentIndex(currentIndex); - DataFilesPage *previousPage = dynamic_cast(pagesWidget->widget(previousIndex)); - DataFilesPage *currentPage = dynamic_cast(pagesWidget->widget(currentIndex)); + // DataFilesPage *previousPage = dynamic_cast(pagesWidget->widget(previousIndex)); + // DataFilesPage *currentPage = dynamic_cast(pagesWidget->widget(currentIndex)); -// //special call to update/save data files page list view when it's displayed/hidden. -// if (previousPage) -// { -// if (previousPage->objectName() == "DataFilesPage") -// previousPage->saveSettings(); -// } -// else if (currentPage) -// { -// if (currentPage->objectName() == "DataFilesPage") -// currentPage->loadSettings(); -// } + // //special call to update/save data files page list view when it's displayed/hidden. + // if (previousPage) + // { + // if (previousPage->objectName() == "DataFilesPage") + // previousPage->saveSettings(); + // } + // else if (currentPage) + // { + // if (currentPage->objectName() == "DataFilesPage") + // currentPage->loadSettings(); + // } } bool Launcher::MainDialog::setupLauncherSettings() @@ -363,10 +296,10 @@ bool Launcher::MainDialog::setupLauncherSettings() msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); - msgBox.exec(); + msgBox.setText(tr("
Could not open %0 for reading

\ + Please make sure you have the right permissions \ + and try again.
").arg(file.fileName())); + msgBox.exec(); return false; } QTextStream stream(&file); @@ -380,78 +313,6 @@ bool Launcher::MainDialog::setupLauncherSettings() return true; } -#ifndef WIN32 -bool Launcher::expansions(Launcher::UnshieldThread& cd) -{ - if(cd.BloodmoonDone()) - { - cd.Done(); - return false; - } - - QMessageBox expansionsBox; - expansionsBox.setText(QObject::tr("
Would you like to install expansions now ? (make sure you have the disc)
\ - If you want to install both Bloodmoon and Tribunal, you have to install Tribunal first.
")); - - QAbstractButton* tribunalButton = NULL; - if(!cd.TribunalDone()) - tribunalButton = expansionsBox.addButton(QObject::tr("&Tribunal"), QMessageBox::ActionRole); - - QAbstractButton* bloodmoonButton = expansionsBox.addButton(QObject::tr("&Bloodmoon"), QMessageBox::ActionRole); - QAbstractButton* noneButton = expansionsBox.addButton(QObject::tr("&None"), QMessageBox::ActionRole); - - expansionsBox.exec(); - - if(expansionsBox.clickedButton() == noneButton) - { - cd.Done(); - return false; - } - else if(expansionsBox.clickedButton() == tribunalButton) - { - - TextSlotMsgBox cdbox; - cdbox.setStandardButtons(QMessageBox::Cancel); - - QObject::connect(&cd,SIGNAL(signalGUI(const QString&)), &cdbox, SLOT(setTextSlot(const QString&))); - QObject::connect(&cd,SIGNAL(close()), &cdbox, SLOT(reject())); - - cd.SetTribunalPath( - QFileDialog::getOpenFileName( - NULL, - QObject::tr("Select data1.hdr from Tribunal Installation CD (Tribunal/data1.hdr on GOTY CDs)"), - QDir::currentPath(), - QString(QObject::tr("Installshield hdr file (*.hdr)"))).toUtf8().constData()); - - cd.start(); - cdbox.exec(); - } - else if(expansionsBox.clickedButton() == bloodmoonButton) - { - - TextSlotMsgBox cdbox; - cdbox.setStandardButtons(QMessageBox::Cancel); - - QObject::connect(&cd,SIGNAL(signalGUI(const QString&)), &cdbox, SLOT(setTextSlot(const QString&))); - QObject::connect(&cd,SIGNAL(close()), &cdbox, SLOT(reject())); - - cd.SetBloodmoonPath( - QFileDialog::getOpenFileName( - NULL, - QObject::tr("Select data1.hdr from Bloodmoon Installation CD (Bloodmoon/data1.hdr on GOTY CDs)"), - QDir::currentPath(), - QString(QObject::tr("Installshield hdr file (*.hdr)"))).toUtf8().constData()); - - cd.start(); - cdbox.exec(); - } - - - - return true; -} -#endif // WIN32 - bool Launcher::MainDialog::setupGameSettings() { QString userPath = QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str()); @@ -470,7 +331,7 @@ bool Launcher::MainDialog::setupGameSettings() msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ + msgBox.setText(tr("
Could not open %0 for reading

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); msgBox.exec(); @@ -498,7 +359,7 @@ bool Launcher::MainDialog::setupGameSettings() msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ + msgBox.setText(tr("
Could not open %0 for reading

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); msgBox.exec(); @@ -530,63 +391,35 @@ bool Launcher::MainDialog::setupGameSettings() msgBox.setWindowTitle(tr("Error detecting Morrowind installation")); msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Cancel); - msgBox.setText(QObject::tr("
Could not find the Data Files location

\ + msgBox.setText(tr("
Could not find the Data Files location

\ The directory containing the data files was not found.

\ Press \"Browse...\" to specify the location manually.
")); - QAbstractButton *dirSelectButton = - msgBox.addButton(QObject::tr("Browse to &Install..."), QMessageBox::ActionRole); + QAbstractButton *browseButton = + msgBox.addButton(tr("Browse..."), QMessageBox::ActionRole); - #ifndef WIN32 - QAbstractButton *cdSelectButton = - msgBox.addButton(QObject::tr("Browse to &CD..."), QMessageBox::ActionRole); - #endif + QAbstractButton *wizardButton = + msgBox.addButton(tr("Run &Installation Wizard..."), QMessageBox::ActionRole); - - msgBox.exec(); + msgBox.exec(); QString selectedFile; - if (msgBox.clickedButton() == dirSelectButton) { + if (msgBox.clickedButton() == browseButton) + { selectedFile = QFileDialog::getOpenFileName( - NULL, - QObject::tr("Select master file"), + this, + tr("Select master file"), QDir::currentPath(), - QString(tr("Morrowind master file (*.esm)"))); + tr("Morrowind master file (*.esm)")); } - #ifndef WIN32 - else if(msgBox.clickedButton() == cdSelectButton) { - UnshieldThread cd; - - { - TextSlotMsgBox cdbox; - cdbox.setStandardButtons(QMessageBox::Cancel); - - QObject::connect(&cd,SIGNAL(signalGUI(const QString&)), &cdbox, SLOT(setTextSlot(const QString&))); - QObject::connect(&cd,SIGNAL(close()), &cdbox, SLOT(reject())); - - cd.SetMorrowindPath( - QFileDialog::getOpenFileName( - NULL, - QObject::tr("Select data1.hdr from Morrowind Installation CD"), - QDir::currentPath(), - QString(tr("Installshield hdr file (*.hdr)"))).toUtf8().constData()); - - cd.SetOutputPath( - QFileDialog::getExistingDirectory( - NULL, - QObject::tr("Select where to extract files to"), - QDir::currentPath(), - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks).toUtf8().constData()); - - cd.start(); - cdbox.exec(); + else if (msgBox.clickedButton() == wizardButton) + { + if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) { + return false; + } else { + return true; } - - while(expansions(cd)); - - selectedFile = QString::fromUtf8(cd.GetMWEsmPath().c_str()); } - #endif // WIN32 if (selectedFile.isEmpty()) return false; // Cancel was clicked; @@ -616,7 +449,7 @@ bool Launcher::MainDialog::setupGraphicsSettings() msgBox.setWindowTitle(tr("Error reading OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not find settings-default.cfg

\ + msgBox.setText(tr("
Could not find settings-default.cfg

\ The problem may be due to an incomplete installation of OpenMW.
\ Reinstalling OpenMW may resolve the problem.")); msgBox.exec(); @@ -638,7 +471,7 @@ bool Launcher::MainDialog::setupGraphicsSettings() msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ + msgBox.setText(tr("
Could not open %0 for reading

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); msgBox.exec(); @@ -705,8 +538,8 @@ bool Launcher::MainDialog::writeSettings() msgBox.setText(tr("
Could not create %0

\ Please make sure you have the right permissions \ and try again.
").arg(userPath)); - msgBox.exec(); - return false; + msgBox.exec(); + return false; } } @@ -722,8 +555,8 @@ bool Launcher::MainDialog::writeSettings() msgBox.setText(tr("
Could not open or create %0 for writing

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); - msgBox.exec(); - return false; + msgBox.exec(); + return false; } QTextStream stream(&file); @@ -744,8 +577,8 @@ bool Launcher::MainDialog::writeSettings() msgBox.setText(tr("
Could not open or create %0 for writing

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); - msgBox.exec(); - return false; + msgBox.exec(); + return false; } stream.setDevice(&file); @@ -766,8 +599,8 @@ bool Launcher::MainDialog::writeSettings() msgBox.setText(tr("
Could not open or create %0 for writing

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); - msgBox.exec(); - return false; + msgBox.exec(); + return false; } stream.setDevice(&file); @@ -786,26 +619,38 @@ void Launcher::MainDialog::closeEvent(QCloseEvent *event) event->accept(); } +void Launcher::MainDialog::wizardStarted() +{ + qDebug() << "wizard started!"; +} + +void Launcher::MainDialog::wizardFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + if (exitCode != 0 || exitStatus == QProcess::CrashExit) + return qApp->quit(); + + reloadSettings(); + show(); +} + void Launcher::MainDialog::play() { - if (!writeSettings()) { - qApp->quit(); - return; - } + 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; + 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("openmw"), true)) - qApp->quit(); + return qApp->quit(); } diff --git a/apps/launcher/maindialog.hpp b/apps/launcher/maindialog.hpp index fefbaecaf9..0708f70026 100644 --- a/apps/launcher/maindialog.hpp +++ b/apps/launcher/maindialog.hpp @@ -2,6 +2,8 @@ #define MAINDIALOG_H #include +#include + #ifndef Q_MOC_RUN #include #endif @@ -51,6 +53,10 @@ namespace Launcher void changePage(QListWidgetItem *current, QListWidgetItem *previous); void play(); + private slots: + void wizardStarted(); + void wizardFinished(int exitCode, QProcess::ExitStatus exitStatus); + private: void createIcons(); void createPages(); @@ -73,6 +79,7 @@ namespace Launcher SettingsPage *mSettingsPage; Process::ProcessInvoker *mGameInvoker; + Process::ProcessInvoker *mWizardInvoker; Files::ConfigurationManager mCfgMgr; diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index fe9369769b..45e0f72a44 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -172,6 +172,8 @@ void Launcher::SettingsPage::on_browseButton_clicked() void Launcher::SettingsPage::wizardStarted() { + mMain->hide(); // Hide the launcher + qDebug() << "wizard started!"; wizardButton->setEnabled(false); } @@ -180,10 +182,12 @@ void Launcher::SettingsPage::wizardFinished(int exitCode, QProcess::ExitStatus e { qDebug() << "wizard finished!"; if (exitCode != 0 || exitStatus == QProcess::CrashExit) - return; + return qApp->quit(); mMain->reloadSettings(); wizardButton->setEnabled(true); + + mMain->show(); // Show the launcher again } void Launcher::SettingsPage::importerStarted() diff --git a/apps/launcher/unshieldthread.cpp b/apps/launcher/unshieldthread.cpp deleted file mode 100644 index 3d0f2e5da6..0000000000 --- a/apps/launcher/unshieldthread.cpp +++ /dev/null @@ -1,521 +0,0 @@ -#include "unshieldthread.hpp" - -#include -#include - -namespace bfs = boost::filesystem; - -namespace -{ - static bool make_sure_directory_exists(bfs::path directory) - { - - if(!bfs::exists(directory)) - { - bfs::create_directories(directory); - } - - return bfs::exists(directory); - } - - void fill_path(bfs::path& path, const std::string& name) - { - size_t start = 0; - - size_t i; - for(i = 0; i < name.length(); i++) - { - switch(name[i]) - { - case '\\': - path /= name.substr(start, i-start); - start = i+1; - break; - } - } - - path /= name.substr(start, i-start); - } - - std::string get_setting(const std::string& category, const std::string& setting, const std::string& inx) - { - size_t start = inx.find(category); - start = inx.find(setting, start) + setting.length() + 3; - - size_t end = inx.find("!", start); - - return inx.substr(start, end-start); - } - - std::string read_to_string(const bfs::path& path) - { - bfs::ifstream strstream(path, std::ios::in | std::ios::binary); - std::string str; - - strstream.seekg(0, std::ios::end); - str.resize(strstream.tellg()); - strstream.seekg(0, std::ios::beg); - strstream.read(&str[0], str.size()); - strstream.close(); - - return str; - } - - void add_setting(const std::string& category, const std::string& setting, const std::string& val, std::string& ini) - { - size_t loc; - loc = ini.find("[" + category + "]"); - - // If category is not found, create it - if(loc == std::string::npos) - { - loc = ini.size() + 2; - ini += ("\r\n[" + category + "]\r\n"); - } - - loc += category.length() +2 +2; - ini.insert(loc, setting + "=" + val + "\r\n"); - } - - #define FIX(setting) add_setting(category, setting, get_setting(category, setting, inx), ini) - - void bloodmoon_fix_ini(std::string& ini, const bfs::path inxPath) - { - std::string inx = read_to_string(inxPath); - - // Remove this one setting (the only one actually changed by bloodmoon, as opposed to just adding new ones) - size_t start = ini.find("[Weather Blight]"); - start = ini.find("Ambient Loop Sound ID", start); - size_t end = ini.find("\r\n", start) +2; - ini.erase(start, end-start); - - std::string category; - - category = "General"; - { - FIX("Werewolf FOV"); - } - category = "Moons"; - { - FIX("Script Color"); - } - category = "Weather"; - { - FIX("Snow Ripples"); - FIX("Snow Ripple Radius"); - FIX("Snow Ripples Per Flake"); - FIX("Snow Ripple Scale"); - FIX("Snow Ripple Speed"); - FIX("Snow Gravity Scale"); - FIX("Snow High Kill"); - FIX("Snow Low Kill"); - } - category = "Weather Blight"; - { - FIX("Ambient Loop Sound ID"); - } - category = "Weather Snow"; - { - FIX("Sky Sunrise Color"); - FIX("Sky Day Color"); - FIX("Sky Sunset Color"); - FIX("Sky Night Color"); - FIX("Fog Sunrise Color"); - FIX("Fog Day Color"); - FIX("Fog Sunset Color"); - FIX("Fog Night Color"); - FIX("Ambient Sunrise Color"); - FIX("Ambient Day Color"); - FIX("Ambient Sunset Color"); - FIX("Ambient Night Color"); - FIX("Sun Sunrise Color"); - FIX("Sun Day Color"); - FIX("Sun Sunset Color"); - FIX("Sun Night Color"); - FIX("Sun Disc Sunset Color"); - FIX("Transition Delta"); - FIX("Land Fog Day Depth"); - FIX("Land Fog Night Depth"); - FIX("Clouds Maximum Percent"); - FIX("Wind Speed"); - FIX("Cloud Speed"); - FIX("Glare View"); - FIX("Cloud Texture"); - FIX("Ambient Loop Sound ID"); - FIX("Snow Threshold"); - FIX("Snow Diameter"); - FIX("Snow Height Min"); - FIX("Snow Height Max"); - FIX("Snow Entrance Speed"); - FIX("Max Snowflakes"); - } - category = "Weather Blizzard"; - { - FIX("Sky Sunrise Color"); - FIX("Sky Day Color"); - FIX("Sky Sunset Color"); - FIX("Sky Night Color"); - FIX("Fog Sunrise Color"); - FIX("Fog Day Color"); - FIX("Fog Sunset Color"); - FIX("Fog Night Color"); - FIX("Ambient Sunrise Color"); - FIX("Ambient Day Color"); - FIX("Ambient Sunset Color"); - FIX("Ambient Night Color"); - FIX("Sun Sunrise Color"); - FIX("Sun Day Color"); - FIX("Sun Sunset Color"); - FIX("Sun Night Color"); - FIX("Sun Disc Sunset Color"); - FIX("Transition Delta"); - FIX("Land Fog Day Depth"); - FIX("Land Fog Night Depth"); - FIX("Clouds Maximum Percent"); - FIX("Wind Speed"); - FIX("Cloud Speed"); - FIX("Glare View"); - FIX("Cloud Texture"); - FIX("Ambient Loop Sound ID"); - FIX("Storm Threshold"); - } - } - - - void fix_ini(const bfs::path& output_dir, bfs::path cdPath, bool tribunal, bool bloodmoon) - { - bfs::path ini_path = output_dir; - ini_path /= "Morrowind.ini"; - - std::string ini = read_to_string(ini_path.string()); - - if(tribunal) - { - add_setting("Game Files", "GameFile1", "Tribunal.esm", ini); - add_setting("Archives", "Archive 0", "Tribunal.bsa", ini); - } - if(bloodmoon) - { - bloodmoon_fix_ini(ini, cdPath / "setup.inx"); - add_setting("Game Files", "GameFile2", "Bloodmoon.esm", ini); - add_setting("Archives", "Archive 1", "Bloodmoon.bsa", ini); - } - - bfs::ofstream inistream((ini_path)); - inistream << ini; - inistream.close(); - } - - void installToPath(const bfs::path& from, const bfs::path& to, bool copy = false) - { - make_sure_directory_exists(to); - - for ( bfs::directory_iterator end, dir(from); dir != end; ++dir ) - { - if(bfs::is_directory(dir->path())) - installToPath(dir->path(), to / dir->path().filename(), copy); - else - { - if(copy) - { - bfs::path dest = to / dir->path().filename(); - if(bfs::exists(dest)) - bfs::remove_all(dest); - bfs::copy_file(dir->path(), dest); - } - else - bfs::rename(dir->path(), to / dir->path().filename()); - } - } - } - - bfs::path findFile(const bfs::path& in, std::string filename, bool recursive = true) - { - if(recursive) - { - for ( bfs::recursive_directory_iterator end, dir(in); dir != end; ++dir ) - { - if(Misc::StringUtils::ciEqual(dir->path().filename().string(), filename)) - return dir->path(); - } - } - else - { - for ( bfs::directory_iterator end, dir(in); dir != end; ++dir ) - { - if(Misc::StringUtils::ciEqual(dir->path().filename().string(), filename)) - return dir->path(); - } - } - - return ""; - } - - bool contains(const bfs::path& in, std::string filename) - { - for(bfs::directory_iterator end, dir(in); dir != end; ++dir) - { - if(Misc::StringUtils::ciEqual(dir->path().filename().string(), filename)) - return true; - } - - return false; - } - - time_t getTime(const char* time) - { - struct tm tms; - memset(&tms, 0, sizeof(struct tm)); - strptime(time, "%d %B %Y", &tms); - return mktime(&tms); - } - - // Some cds have cab files which have the Data Files subfolders outside the Data Files folder - void install_dfiles_outside(const bfs::path& from, const bfs::path& dFiles) - { - bfs::path fonts = findFile(from, "fonts", false); - if(fonts.string() != "") - installToPath(fonts, dFiles / "Fonts"); - - bfs::path music = findFile(from, "music", false); - if(music.string() != "") - installToPath(music, dFiles / "Music"); - - bfs::path sound = findFile(from, "sound", false); - if(sound.string() != "") - installToPath(sound, dFiles / "Sound"); - - bfs::path splash = findFile(from, "splash", false); - if(splash.string() != "") - installToPath(splash, dFiles / "Splash"); - } - -} - -bool Launcher::UnshieldThread::SetMorrowindPath(const std::string& path) -{ - mMorrowindPath = path; - return true; -} - -bool Launcher::UnshieldThread::SetTribunalPath(const std::string& path) -{ - mTribunalPath = path; - return true; -} - -bool Launcher::UnshieldThread::SetBloodmoonPath(const std::string& path) -{ - mBloodmoonPath = path; - return true; -} - -void Launcher::UnshieldThread::SetOutputPath(const std::string& path) -{ - mOutputPath = path; -} - -bool Launcher::UnshieldThread::extract_file(Unshield* unshield, bfs::path output_dir, const char* prefix, int index) -{ - bool success; - bfs::path dirname; - bfs::path filename; - int directory = unshield_file_directory(unshield, index); - - dirname = output_dir; - - if (prefix && prefix[0]) - dirname /= prefix; - - if (directory >= 0) - { - const char* tmp = unshield_directory_name(unshield, directory); - if (tmp && tmp[0]) - fill_path(dirname, tmp); - } - - make_sure_directory_exists(dirname); - - filename = dirname; - filename /= unshield_file_name(unshield, index); - - emit signalGUI(QString("Extracting: ") + QString(filename.c_str())); - - success = unshield_file_save(unshield, index, filename.c_str()); - - if (!success) - bfs::remove(filename); - - return success; -} - -void Launcher::UnshieldThread::extract_cab(const bfs::path& cab, const bfs::path& output_dir, bool extract_ini) -{ - Unshield * unshield; - unshield = unshield_open(cab.c_str()); - - int i; - for (i = 0; i < unshield_file_group_count(unshield); i++) - { - UnshieldFileGroup* file_group = unshield_file_group_get(unshield, i); - - for (size_t j = file_group->first_file; j <= file_group->last_file; j++) - { - if (unshield_file_is_valid(unshield, j)) - extract_file(unshield, output_dir, file_group->name, j); - } - } - unshield_close(unshield); -} - - -bool Launcher::UnshieldThread::extract() -{ - bfs::path outputDataFilesDir = mOutputPath; - outputDataFilesDir /= "Data Files"; - bfs::path extractPath = mOutputPath; - extractPath /= "extract-temp"; - - if(!mMorrowindDone && mMorrowindPath.string().length() > 0) - { - mMorrowindDone = true; - - bfs::path mwExtractPath = extractPath / "morrowind"; - extract_cab(mMorrowindPath, mwExtractPath, true); - - bfs::path dFilesDir = findFile(mwExtractPath, "morrowind.esm").parent_path(); - - installToPath(dFilesDir, outputDataFilesDir); - - install_dfiles_outside(mwExtractPath, outputDataFilesDir); - - // Videos are often kept uncompressed on the cd - bfs::path videosPath = findFile(mMorrowindPath.parent_path(), "video", false); - if(videosPath.string() != "") - { - emit signalGUI(QString("Installing Videos...")); - installToPath(videosPath, outputDataFilesDir / "Video", true); - } - - bfs::path cdDFiles = findFile(mMorrowindPath.parent_path(), "data files", false); - if(cdDFiles.string() != "") - { - emit signalGUI(QString("Installing Uncompressed Data files from CD...")); - installToPath(cdDFiles, outputDataFilesDir, true); - } - - - bfs::rename(findFile(mwExtractPath, "morrowind.ini"), outputDataFilesDir / "Morrowind.ini"); - - mTribunalDone = contains(outputDataFilesDir, "tribunal.esm"); - mBloodmoonDone = contains(outputDataFilesDir, "bloodmoon.esm"); - - } - - else if(!mTribunalDone && mTribunalPath.string().length() > 0) - { - mTribunalDone = true; - - bfs::path tbExtractPath = extractPath / "tribunal"; - extract_cab(mTribunalPath, tbExtractPath, true); - - bfs::path dFilesDir = findFile(tbExtractPath, "tribunal.esm").parent_path(); - - installToPath(dFilesDir, outputDataFilesDir); - - install_dfiles_outside(tbExtractPath, outputDataFilesDir); - - // Mt GOTY CD has Sounds in a seperate folder from the rest of the data files - bfs::path soundsPath = findFile(tbExtractPath, "sounds", false); - if(soundsPath.string() != "") - installToPath(soundsPath, outputDataFilesDir / "Sounds"); - - bfs::path cdDFiles = findFile(mTribunalPath.parent_path(), "data files", false); - if(cdDFiles.string() != "") - { - emit signalGUI(QString("Installing Uncompressed Data files from CD...")); - installToPath(cdDFiles, outputDataFilesDir, true); - } - - mBloodmoonDone = contains(outputDataFilesDir, "bloodmoon.esm"); - - fix_ini(outputDataFilesDir, bfs::path(mTribunalPath).parent_path(), mTribunalDone, mBloodmoonDone); - } - - else if(!mBloodmoonDone && mBloodmoonPath.string().length() > 0) - { - mBloodmoonDone = true; - - bfs::path bmExtractPath = extractPath / "bloodmoon"; - extract_cab(mBloodmoonPath, bmExtractPath, true); - - bfs::path dFilesDir = findFile(bmExtractPath, "bloodmoon.esm").parent_path(); - - installToPath(dFilesDir, outputDataFilesDir); - - install_dfiles_outside(bmExtractPath, outputDataFilesDir); - - // My GOTY CD contains a folder within cab files called Tribunal patch, - // which contains Tribunal.esm - bfs::path tbPatchPath = findFile(bmExtractPath, "tribunal.esm"); - if(tbPatchPath.string() != "") - bfs::rename(tbPatchPath, outputDataFilesDir / "Tribunal.esm"); - - bfs::path cdDFiles = findFile(mBloodmoonPath.parent_path(), "data files", false); - if(cdDFiles.string() != "") - { - emit signalGUI(QString("Installing Uncompressed Data files from CD...")); - installToPath(cdDFiles, outputDataFilesDir, true); - } - - fix_ini(outputDataFilesDir, bfs::path(mBloodmoonPath).parent_path(), false, mBloodmoonDone); - } - - - return true; -} - -void Launcher::UnshieldThread::Done() -{ - // Get rid of unnecessary files - bfs::remove_all(mOutputPath / "extract-temp"); - - // Set modified time to release dates, to preserve load order - if(mMorrowindDone) - bfs::last_write_time(findFile(mOutputPath, "morrowind.esm"), getTime("1 May 2002")); - - if(mTribunalDone) - bfs::last_write_time(findFile(mOutputPath, "tribunal.esm"), getTime("6 November 2002")); - - if(mBloodmoonDone) - bfs::last_write_time(findFile(mOutputPath, "bloodmoon.esm"), getTime("3 June 2003")); -} - -std::string Launcher::UnshieldThread::GetMWEsmPath() -{ - return findFile(mOutputPath / "Data Files", "morrowind.esm").string(); -} - -bool Launcher::UnshieldThread::TribunalDone() -{ - return mTribunalDone; -} - -bool Launcher::UnshieldThread::BloodmoonDone() -{ - return mBloodmoonDone; -} - -void Launcher::UnshieldThread::run() -{ - extract(); - emit close(); -} - -Launcher::UnshieldThread::UnshieldThread() -{ - unshield_set_log_level(0); - mMorrowindDone = false; - mTribunalDone = false; - mBloodmoonDone = false; -} diff --git a/apps/launcher/unshieldthread.hpp b/apps/launcher/unshieldthread.hpp deleted file mode 100644 index de6a32b442..0000000000 --- a/apps/launcher/unshieldthread.hpp +++ /dev/null @@ -1,58 +0,0 @@ -#ifndef UNSHIELD_THREAD_H -#define UNSHIELD_THREAD_H - -#include - -#include - -#include - -namespace Launcher -{ - class UnshieldThread : public QThread - { - Q_OBJECT - - public: - bool SetMorrowindPath(const std::string& path); - bool SetTribunalPath(const std::string& path); - bool SetBloodmoonPath(const std::string& path); - - void SetOutputPath(const std::string& path); - - bool extract(); - - bool TribunalDone(); - bool BloodmoonDone(); - - void Done(); - - std::string GetMWEsmPath(); - - UnshieldThread(); - - private: - - void extract_cab(const boost::filesystem::path& cab, const boost::filesystem::path& output_dir, bool extract_ini = false); - bool extract_file(Unshield* unshield, boost::filesystem::path output_dir, const char* prefix, int index); - - boost::filesystem::path mMorrowindPath; - boost::filesystem::path mTribunalPath; - boost::filesystem::path mBloodmoonPath; - - bool mMorrowindDone; - bool mTribunalDone; - bool mBloodmoonDone; - - boost::filesystem::path mOutputPath; - - - protected: - virtual void run(); - - signals: - void signalGUI(QString); - void close(); - }; -} -#endif diff --git a/apps/launcher/utils/checkablemessagebox.cpp b/apps/launcher/utils/checkablemessagebox.cpp deleted file mode 100644 index 2f775af57a..0000000000 --- a/apps/launcher/utils/checkablemessagebox.cpp +++ /dev/null @@ -1,258 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). -** Contact: http://www.qt-project.org/legal -** -** This file is part of Qt Creator. -** -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and Digia. For licensing terms and -** conditions see http://qt.digia.com/licensing. For further information -** use the contact form at http://qt.digia.com/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Digia gives you certain additional -** rights. These rights are described in the Digia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -****************************************************************************/ - -#include "checkablemessagebox.hpp" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -/*! - \class Utils::CheckableMessageBox - - \brief A messagebox suitable for questions with a - "Do not ask me again" checkbox. - - Emulates the QMessageBox API with - static conveniences. The message label can open external URLs. -*/ -Launcher::CheckableMessageBoxPrivate::CheckableMessageBoxPrivate(QDialog *q) - : clickedButton(0) -{ - QSizePolicy sizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); - - pixmapLabel = new QLabel(q); - sizePolicy.setHorizontalStretch(0); - sizePolicy.setVerticalStretch(0); - sizePolicy.setHeightForWidth(pixmapLabel->sizePolicy().hasHeightForWidth()); - pixmapLabel->setSizePolicy(sizePolicy); - pixmapLabel->setVisible(false); - - QSpacerItem *pixmapSpacer = - new QSpacerItem(0, 5, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding); - - messageLabel = new QLabel(q); - messageLabel->setMinimumSize(QSize(300, 0)); - messageLabel->setWordWrap(true); - messageLabel->setOpenExternalLinks(true); - messageLabel->setTextInteractionFlags(Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse); - - QSpacerItem *checkBoxRightSpacer = - new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Minimum); - QSpacerItem *buttonSpacer = - new QSpacerItem(0, 1, QSizePolicy::Minimum, QSizePolicy::Minimum); - - checkBox = new QCheckBox(q); - checkBox->setText(Launcher::CheckableMessageBox::tr("Do not ask again")); - - buttonBox = new QDialogButtonBox(q); - buttonBox->setOrientation(Qt::Horizontal); - buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok); - - QVBoxLayout *verticalLayout = new QVBoxLayout(); - verticalLayout->addWidget(pixmapLabel); - verticalLayout->addItem(pixmapSpacer); - - QHBoxLayout *horizontalLayout_2 = new QHBoxLayout(); - horizontalLayout_2->addLayout(verticalLayout); - horizontalLayout_2->addWidget(messageLabel); - - QHBoxLayout *horizontalLayout = new QHBoxLayout(); - horizontalLayout->addWidget(checkBox); - horizontalLayout->addItem(checkBoxRightSpacer); - - QVBoxLayout *verticalLayout_2 = new QVBoxLayout(q); - verticalLayout_2->addLayout(horizontalLayout_2); - verticalLayout_2->addLayout(horizontalLayout); - verticalLayout_2->addItem(buttonSpacer); - verticalLayout_2->addWidget(buttonBox); -} - -Launcher::CheckableMessageBox::CheckableMessageBox(QWidget *parent) : - QDialog(parent), - d(new Launcher::CheckableMessageBoxPrivate(this)) -{ - setModal(true); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - connect(d->buttonBox, SIGNAL(accepted()), SLOT(accept())); - connect(d->buttonBox, SIGNAL(rejected()), SLOT(reject())); - connect(d->buttonBox, SIGNAL(clicked(QAbstractButton*)), - SLOT(slotClicked(QAbstractButton*))); -} - -Launcher::CheckableMessageBox::~CheckableMessageBox() -{ - delete d; -} - -void Launcher::CheckableMessageBox::slotClicked(QAbstractButton *b) -{ - d->clickedButton = b; -} - -QAbstractButton *Launcher::CheckableMessageBox::clickedButton() const -{ - return d->clickedButton; -} - -QDialogButtonBox::StandardButton Launcher::CheckableMessageBox::clickedStandardButton() const -{ - if (d->clickedButton) - return d->buttonBox->standardButton(d->clickedButton); - return QDialogButtonBox::NoButton; -} - -QString Launcher::CheckableMessageBox::text() const -{ - return d->messageLabel->text(); -} - -void Launcher::CheckableMessageBox::setText(const QString &t) -{ - d->messageLabel->setText(t); -} - -QPixmap Launcher::CheckableMessageBox::iconPixmap() const -{ - if (const QPixmap *p = d->pixmapLabel->pixmap()) - return QPixmap(*p); - return QPixmap(); -} - -void Launcher::CheckableMessageBox::setIconPixmap(const QPixmap &p) -{ - d->pixmapLabel->setPixmap(p); - d->pixmapLabel->setVisible(!p.isNull()); -} - -bool Launcher::CheckableMessageBox::isChecked() const -{ - return d->checkBox->isChecked(); -} - -void Launcher::CheckableMessageBox::setChecked(bool s) -{ - d->checkBox->setChecked(s); -} - -QString Launcher::CheckableMessageBox::checkBoxText() const -{ - return d->checkBox->text(); -} - -void Launcher::CheckableMessageBox::setCheckBoxText(const QString &t) -{ - d->checkBox->setText(t); -} - -bool Launcher::CheckableMessageBox::isCheckBoxVisible() const -{ - return d->checkBox->isVisible(); -} - -void Launcher::CheckableMessageBox::setCheckBoxVisible(bool v) -{ - d->checkBox->setVisible(v); -} - -QDialogButtonBox::StandardButtons Launcher::CheckableMessageBox::standardButtons() const -{ - return d->buttonBox->standardButtons(); -} - -void Launcher::CheckableMessageBox::setStandardButtons(QDialogButtonBox::StandardButtons s) -{ - d->buttonBox->setStandardButtons(s); -} - -QPushButton *Launcher::CheckableMessageBox::button(QDialogButtonBox::StandardButton b) const -{ - return d->buttonBox->button(b); -} - -QPushButton *Launcher::CheckableMessageBox::addButton(const QString &text, QDialogButtonBox::ButtonRole role) -{ - return d->buttonBox->addButton(text, role); -} - -QDialogButtonBox::StandardButton Launcher::CheckableMessageBox::defaultButton() const -{ - foreach (QAbstractButton *b, d->buttonBox->buttons()) - if (QPushButton *pb = qobject_cast(b)) - if (pb->isDefault()) - return d->buttonBox->standardButton(pb); - return QDialogButtonBox::NoButton; -} - -void Launcher::CheckableMessageBox::setDefaultButton(QDialogButtonBox::StandardButton s) -{ - if (QPushButton *b = d->buttonBox->button(s)) { - b->setDefault(true); - b->setFocus(); - } -} - -QDialogButtonBox::StandardButton -Launcher::CheckableMessageBox::question(QWidget *parent, - const QString &title, - const QString &question, - const QString &checkBoxText, - bool *checkBoxSetting, - QDialogButtonBox::StandardButtons buttons, - QDialogButtonBox::StandardButton defaultButton) -{ - CheckableMessageBox mb(parent); - mb.setWindowTitle(title); - mb.setIconPixmap(QMessageBox::standardIcon(QMessageBox::Question)); - mb.setText(question); - mb.setCheckBoxText(checkBoxText); - mb.setChecked(*checkBoxSetting); - mb.setStandardButtons(buttons); - mb.setDefaultButton(defaultButton); - mb.exec(); - *checkBoxSetting = mb.isChecked(); - return mb.clickedStandardButton(); -} - -QMessageBox::StandardButton Launcher::CheckableMessageBox::dialogButtonBoxToMessageBoxButton(QDialogButtonBox::StandardButton db) -{ - return static_cast(int(db)); -} diff --git a/apps/launcher/utils/checkablemessagebox.hpp b/apps/launcher/utils/checkablemessagebox.hpp deleted file mode 100644 index 09a501b9c2..0000000000 --- a/apps/launcher/utils/checkablemessagebox.hpp +++ /dev/null @@ -1,116 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). -** Contact: http://www.qt-project.org/legal -** -** This file is part of Qt Creator. -** -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and Digia. For licensing terms and -** conditions see http://qt.digia.com/licensing. For further information -** use the contact form at http://qt.digia.com/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Digia gives you certain additional -** rights. These rights are described in the Digia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -****************************************************************************/ - -#ifndef CHECKABLEMESSAGEBOX_HPP -#define CHECKABLEMESSAGEBOX_HPP - -#include -#include -#include - -class QCheckBox; - -namespace Launcher -{ - class CheckableMessageBoxPrivate - { - public: - - QLabel *pixmapLabel; - QLabel *messageLabel; - QCheckBox *checkBox; - QDialogButtonBox *buttonBox; - QAbstractButton *clickedButton; - - public: - CheckableMessageBoxPrivate(QDialog *q); - }; - - class CheckableMessageBox : public QDialog - { - Q_OBJECT - Q_PROPERTY(QString text READ text WRITE setText) - Q_PROPERTY(QPixmap iconPixmap READ iconPixmap WRITE setIconPixmap) - Q_PROPERTY(bool isChecked READ isChecked WRITE setChecked) - Q_PROPERTY(QString checkBoxText READ checkBoxText WRITE setCheckBoxText) - Q_PROPERTY(QDialogButtonBox::StandardButtons buttons READ standardButtons WRITE setStandardButtons) - Q_PROPERTY(QDialogButtonBox::StandardButton defaultButton READ defaultButton WRITE setDefaultButton) - - public: - explicit CheckableMessageBox(QWidget *parent); - virtual ~CheckableMessageBox(); - - static QDialogButtonBox::StandardButton - question(QWidget *parent, - const QString &title, - const QString &question, - const QString &checkBoxText, - bool *checkBoxSetting, - QDialogButtonBox::StandardButtons buttons = QDialogButtonBox::Yes|QDialogButtonBox::No, - QDialogButtonBox::StandardButton defaultButton = QDialogButtonBox::No); - - QString text() const; - void setText(const QString &); - - bool isChecked() const; - void setChecked(bool s); - - QString checkBoxText() const; - void setCheckBoxText(const QString &); - - bool isCheckBoxVisible() const; - void setCheckBoxVisible(bool); - - QDialogButtonBox::StandardButtons standardButtons() const; - void setStandardButtons(QDialogButtonBox::StandardButtons s); - QPushButton *button(QDialogButtonBox::StandardButton b) const; - QPushButton *addButton(const QString &text, QDialogButtonBox::ButtonRole role); - - QDialogButtonBox::StandardButton defaultButton() const; - void setDefaultButton(QDialogButtonBox::StandardButton s); - - // See static QMessageBox::standardPixmap() - QPixmap iconPixmap() const; - void setIconPixmap (const QPixmap &p); - - // Query the result - QAbstractButton *clickedButton() const; - QDialogButtonBox::StandardButton clickedStandardButton() const; - - // Conversion convenience - static QMessageBox::StandardButton dialogButtonBoxToMessageBoxButton(QDialogButtonBox::StandardButton); - - private slots: - void slotClicked(QAbstractButton *b); - - private: - CheckableMessageBoxPrivate *d; - }; -} -#endif // CHECKABLEMESSAGEBOX_HPP From 88064f49469f06b2c5b5a2a534ed175c05c560b6 Mon Sep 17 00:00:00 2001 From: pvdk Date: Thu, 29 May 2014 20:35:07 +0200 Subject: [PATCH 057/303] Fix for MOC error due to Boost includes --- apps/wizard/mainwizard.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index 6b8c4931fe..c22f20c259 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -7,7 +7,9 @@ #include +#ifndef Q_MOC_RUN #include +#endif #include #include From 2a6a26c0d050daddc31f2883d82c35e3af3d93fd Mon Sep 17 00:00:00 2001 From: pvdk Date: Thu, 29 May 2014 21:02:06 +0200 Subject: [PATCH 058/303] Minor fixes --- apps/wizard/existinginstallationpage.cpp | 8 ++------ apps/wizard/mainwizard.cpp | 2 +- files/ui/wizard/intropage.ui | 10 ++++------ 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 7c5d10a802..7171cb26bb 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -23,12 +23,6 @@ void Wizard::ExistingInstallationPage::initializePage() emptyItem->setFlags(Qt::NoItemFlags); installationsList->addItem(emptyItem); - // Test - if (mWizard->mInstallations.isEmpty()) { - qDebug() << "crashy crash"; - return; - } - // Add the available installation paths QStringList paths(mWizard->mInstallations.keys()); @@ -133,6 +127,8 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() installationsList->setCurrentItem(items.first()); } + // Update the button + emit completeChanged(); } void Wizard::ExistingInstallationPage::textChanged(const QString &text) diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 92743417e0..33d08b50ad 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -279,7 +279,7 @@ void Wizard::MainWizard::runSettingsImporter() return qApp->quit(); // Re-read the game settings - // setupGameSettings(); + setupGameSettings(); } void Wizard::MainWizard::addInstallation(const QString &path) diff --git a/files/ui/wizard/intropage.ui b/files/ui/wizard/intropage.ui index b06f7f170c..117d2098ef 100644 --- a/files/ui/wizard/intropage.ui +++ b/files/ui/wizard/intropage.ui @@ -6,8 +6,8 @@ 0 0 - 474 - 370 + 472 + 368
@@ -20,7 +20,7 @@ - This Wizard will help you install Morrowindand its add-ons for OpenMW to use. + This Wizard will help you install Morrowind and its add-ons for OpenMW to use. true @@ -29,8 +29,6 @@ - - - + From a390dde818180fe846dda7c5abcb72f38b649666 Mon Sep 17 00:00:00 2001 From: pvdk Date: Thu, 29 May 2014 21:17:49 +0200 Subject: [PATCH 059/303] Fixed importing configurations of existing installations --- apps/launcher/maindialog.cpp | 2 +- apps/wizard/mainwizard.cpp | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 76b9ad3c54..708969516d 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -257,7 +257,7 @@ void Launcher::MainDialog::changePage(QListWidgetItem *current, QListWidgetItem current = previous; int currentIndex = iconWidget->row(current); - int previousIndex = iconWidget->row(previous); +// int previousIndex = iconWidget->row(previous); pagesWidget->setCurrentIndex(currentIndex); diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 33d08b50ad..2469cfa8bf 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -271,7 +271,13 @@ void Wizard::MainWizard::runSettingsImporter() // Now the paths arguments.append(QLatin1String("--ini")); - arguments.append(path + QDir::separator() + QLatin1String("Morrowind.ini")); + + if (field(QLatin1String("installation.new")).toBool() == true) { + arguments.append(path + QDir::separator() + QLatin1String("Morrowind.ini")); + } else { + arguments.append(mInstallations[path].iniPath); + } + arguments.append(QLatin1String("--cfg")); arguments.append(userPath + QLatin1String("openmw.cfg")); From 6348af586e80545cae112850ab369b1a93ba45c9 Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 30 May 2014 02:12:48 +0200 Subject: [PATCH 060/303] Fixed some problems with the launcher and the wizard --- apps/launcher/datafilespage.cpp | 10 +--- apps/launcher/datafilespage.hpp | 2 - apps/launcher/main.cpp | 10 ++-- apps/launcher/maindialog.cpp | 69 ++++++++++++------------ apps/wizard/existinginstallationpage.cpp | 14 +++-- apps/wizard/mainwizard.cpp | 6 --- 6 files changed, 49 insertions(+), 62 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 6b53d89a44..17951f2e49 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -36,7 +36,7 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: this, SLOT(updateOkButton(QString))); buildView(); - setupDataFiles(); + loadSettings(); } void Launcher::DataFilesPage::buildView() @@ -67,7 +67,7 @@ void Launcher::DataFilesPage::buildView() this, SLOT (slotProfileChangedByUser(QString, QString))); } -void Launcher::DataFilesPage::setupDataFiles() +bool Launcher::DataFilesPage::loadSettings() { QStringList paths = mGameSettings.getDataDirs(); @@ -79,12 +79,6 @@ void Launcher::DataFilesPage::setupDataFiles() if (!mDataLocal.isEmpty()) mSelector->addFiles(mDataLocal); - loadSettings(); -} - -bool Launcher::DataFilesPage::loadSettings() -{ - QStringList paths = mGameSettings.getDataDirs(); paths.insert (0, mDataLocal); PathIterator pathIterator (paths); diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index 908394ae15..5beeb0e03a 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -41,8 +41,6 @@ namespace Launcher void saveSettings(const QString &profile = ""); bool loadSettings(); - void setupDataFiles(); - signals: void signalProfileChanged (int index); diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index 67320bff0f..da319426d8 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -52,12 +52,12 @@ int main(int argc, char *argv[]) Launcher::MainDialog mainWin; - if (!mainWin.setup()) { + if (!mainWin.showFirstRunDialog()) return 0; - //mainWin.show(); - }/* else { - return 0; - }*/ + +// if (!mainWin.setup()) +// return 0; + int returnValue = app.exec(); SDL_Quit(); diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 708969516d..c66de0b032 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -165,40 +165,44 @@ void Launcher::MainDialog::createPages() bool Launcher::MainDialog::showFirstRunDialog() { - 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); - Q_UNUSED(skipButton); // Surpress compiler unused warning + if (!setupLauncherSettings()) + return false; - msgBox.exec(); - - if (msgBox.clickedButton() == wizardButton) + if (mLauncherSettings.value(QString("General/firstrun"), QString("true")) == QLatin1String("true")) { - if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) { - return false; - } else { - return 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); + + Q_UNUSED(skipButton); // Surpress compiler unused warning + + msgBox.exec(); + + if (msgBox.clickedButton() == wizardButton) + { + if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) { + return false; + } else { + return true; + } } } - show(); - return true; + return setup(); } bool Launcher::MainDialog::setup() { - if (!setupLauncherSettings()) - return false; - if (!setupGameSettings()) return false; @@ -214,17 +218,7 @@ bool Launcher::MainDialog::setup() loadSettings(); - // Check if we need to run the wizard - if (mLauncherSettings.value(QString("General/firstrun"), QString("true")) == QLatin1String("true")) - { - if (!showFirstRunDialog()) { - return false; - } else { - return true; - } - } - - show(); // Show ourselves if the wizard is not being run + show(); return true; } @@ -629,6 +623,9 @@ void Launcher::MainDialog::wizardFinished(int exitCode, QProcess::ExitStatus exi if (exitCode != 0 || exitStatus == QProcess::CrashExit) return qApp->quit(); + if (!setup()) + return; + reloadSettings(); show(); } diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 7171cb26bb..83ea20f5a8 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -15,14 +15,16 @@ Wizard::ExistingInstallationPage::ExistingInstallationPage(QWidget *parent) : setupUi(this); + // Add a placeholder item to the list of installations + QListWidgetItem *emptyItem = new QListWidgetItem(tr("No existing installations detected")); + emptyItem->setFlags(Qt::NoItemFlags); + + installationsList->insertItem(0, emptyItem); + } void Wizard::ExistingInstallationPage::initializePage() { - QListWidgetItem *emptyItem = new QListWidgetItem(tr("No existing installations detected")); - emptyItem->setFlags(Qt::NoItemFlags); - installationsList->addItem(emptyItem); - // Add the available installation paths QStringList paths(mWizard->mInstallations.keys()); @@ -35,7 +37,9 @@ void Wizard::ExistingInstallationPage::initializePage() foreach (const QString &path, paths) { QListWidgetItem *item = new QListWidgetItem(path); - installationsList->addItem(item); + + if (installationsList->findItems(path, Qt::MatchExactly).isEmpty()) + installationsList->addItem(item); } connect(installationsList, SIGNAL(currentTextChanged(QString)), diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 2469cfa8bf..e587d77e54 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -37,9 +37,6 @@ Wizard::MainWizard::MainWizard(QWidget *parent) : setWindowIcon(QIcon(QLatin1String(":/images/openmw-wizard.png"))); setMinimumWidth(550); - // This prevents initializePage() being called multiple times - setOption(QWizard::IndependentPages); - // Set the property for comboboxes to the text instead of index setDefaultProperty("QComboBox", "currentText", "currentIndexChanged"); @@ -283,9 +280,6 @@ void Wizard::MainWizard::runSettingsImporter() if (!mImporterInvoker->startProcess(QLatin1String("mwiniimport"), arguments, false)) return qApp->quit(); - - // Re-read the game settings - setupGameSettings(); } void Wizard::MainWizard::addInstallation(const QString &path) From 0cc7c062624c7ea9a6ad8adf44ca11eacf803ab0 Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 30 May 2014 02:41:09 +0200 Subject: [PATCH 061/303] Ensure the launcher is not shown when the wizard is run --- apps/launcher/main.cpp | 4 +++- apps/launcher/maindialog.cpp | 14 +++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index da319426d8..562f5c7795 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -55,9 +55,11 @@ int main(int argc, char *argv[]) if (!mainWin.showFirstRunDialog()) return 0; -// if (!mainWin.setup()) +// if (!mainWin.setup()) { // return 0; +// } + mainWin.show(); int returnValue = app.exec(); SDL_Quit(); diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index c66de0b032..59e0238279 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -51,6 +51,9 @@ Launcher::MainDialog::MainDialog(QWidget *parent) 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))); @@ -218,7 +221,6 @@ bool Launcher::MainDialog::setup() loadSettings(); - show(); return true; } @@ -416,7 +418,7 @@ bool Launcher::MainDialog::setupGameSettings() } if (selectedFile.isEmpty()) - return false; // Cancel was clicked; + return false; // Cancel was clicked QFileInfo info(selectedFile); @@ -616,6 +618,7 @@ void Launcher::MainDialog::closeEvent(QCloseEvent *event) void Launcher::MainDialog::wizardStarted() { qDebug() << "wizard started!"; + hide(); } void Launcher::MainDialog::wizardFinished(int exitCode, QProcess::ExitStatus exitStatus) @@ -623,11 +626,8 @@ void Launcher::MainDialog::wizardFinished(int exitCode, QProcess::ExitStatus exi if (exitCode != 0 || exitStatus == QProcess::CrashExit) return qApp->quit(); - if (!setup()) - return; - - reloadSettings(); - show(); + if (reloadSettings()) + show(); } void Launcher::MainDialog::play() From fda25f385aa85fc05fe9e9637b709fab7efec849 Mon Sep 17 00:00:00 2001 From: pvdk Date: Fri, 30 May 2014 02:50:17 +0200 Subject: [PATCH 062/303] Fixed segfault when launcher tries to reload settings on pages not yet there --- apps/launcher/maindialog.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 59e0238279..41a23e2464 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -626,6 +626,9 @@ void Launcher::MainDialog::wizardFinished(int exitCode, QProcess::ExitStatus exi if (exitCode != 0 || exitStatus == QProcess::CrashExit) return qApp->quit(); + // HACK: Ensure the pages are created, else segfault + setup(); + if (reloadSettings()) show(); } From 2fa44c3c5f944836f229c4cc3c76b4e826a9193a Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Fri, 24 Oct 2014 09:30:48 +0200 Subject: [PATCH 063/303] removed two issues from the changelog that did not make it into the release --- readme.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/readme.txt b/readme.txt index 254e60f7f6..cb533e9fbb 100644 --- a/readme.txt +++ b/readme.txt @@ -197,14 +197,12 @@ 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 #854: Editor: Add user setting to show status bar 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 #1310: Editor: Rendering User Settings Feature #1545: Targeting console on player Feature #1597: Editor: Render terrain Feature #1695: Editor: add column for CellRef's global variable From a4d0068e2980800a3454ac83168e33d16b892b0e Mon Sep 17 00:00:00 2001 From: MiroslavR Date: Sun, 2 Nov 2014 15:35:14 +0100 Subject: [PATCH 064/303] Make forcegreeting no-op for disabled references (Fixes #2093) --- apps/openmw/mwscript/dialogueextensions.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/openmw/mwscript/dialogueextensions.cpp b/apps/openmw/mwscript/dialogueextensions.cpp index a88c5a1015..563a9dde3a 100644 --- a/apps/openmw/mwscript/dialogueextensions.cpp +++ b/apps/openmw/mwscript/dialogueextensions.cpp @@ -125,6 +125,9 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); + if (!ptr.getRefData().isEnabled()) + return; + MWBase::Environment::get().getDialogueManager()->startDialogue (ptr); } }; From 140013820ba858ac9d46df3f2e78ec1c9237ac87 Mon Sep 17 00:00:00 2001 From: MiroslavR Date: Sun, 2 Nov 2014 15:40:08 +0100 Subject: [PATCH 065/303] Fix invalidated iterator --- apps/openmw/mwmechanics/spells.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwmechanics/spells.cpp b/apps/openmw/mwmechanics/spells.cpp index 681f01f04b..a1b73bc47c 100644 --- a/apps/openmw/mwmechanics/spells.cpp +++ b/apps/openmw/mwmechanics/spells.cpp @@ -70,11 +70,13 @@ namespace MWMechanics if (mPermanentSpellEffects.find(lower) != mPermanentSpellEffects.end()) { MagicEffects & effects = mPermanentSpellEffects[lower]; - for (MagicEffects::Collection::const_iterator effectIt = effects.begin(); effectIt != effects.end(); ++effectIt) + for (MagicEffects::Collection::const_iterator effectIt = effects.begin(); effectIt != effects.end();) { const ESM::MagicEffect * magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectIt->first.mId); if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) - effects.remove(effectIt->first); + effects.remove((effectIt++)->first); + else + ++effectIt; } } mCorprusSpells.erase(corprusIt); From bf40a3bb5dd33538d81b03317af4093d30c8944d Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Mon, 3 Nov 2014 13:18:53 +0100 Subject: [PATCH 066/303] enhanced SceneToolBar tool handling (remove and insert at specific point) --- apps/opencs/view/widget/scenetoolbar.cpp | 15 +++++++++++++-- apps/opencs/view/widget/scenetoolbar.hpp | 6 +++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/opencs/view/widget/scenetoolbar.cpp b/apps/opencs/view/widget/scenetoolbar.cpp index eac9bcec32..f7023b31f3 100644 --- a/apps/opencs/view/widget/scenetoolbar.cpp +++ b/apps/opencs/view/widget/scenetoolbar.cpp @@ -31,9 +31,20 @@ CSVWidget::SceneToolbar::SceneToolbar (int buttonSize, QWidget *parent) connect (focusScene, SIGNAL (activated()), this, SIGNAL (focusSceneRequest())); } -void CSVWidget::SceneToolbar::addTool (SceneTool *tool) +void CSVWidget::SceneToolbar::addTool (SceneTool *tool, SceneTool *insertPoint) { - mLayout->addWidget (tool, 0, Qt::AlignTop); + 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 diff --git a/apps/opencs/view/widget/scenetoolbar.hpp b/apps/opencs/view/widget/scenetoolbar.hpp index 1902ba2bed..8e2c8ab00b 100644 --- a/apps/opencs/view/widget/scenetoolbar.hpp +++ b/apps/opencs/view/widget/scenetoolbar.hpp @@ -25,7 +25,11 @@ namespace CSVWidget SceneToolbar (int buttonSize, QWidget *parent = 0); - void addTool (SceneTool *tool); + /// If insertPoint==0, insert \a tool at the end of the scrollbar. Otherwise + /// insert tool after \a insertPoint. + void addTool (SceneTool *tool, SceneTool *insertPoint = 0); + + void removeTool (SceneTool *tool); int getButtonSize() const; From 0e70315f91ce6a3630f5bc8ca836702c4aeb0c4b Mon Sep 17 00:00:00 2001 From: cc9cii Date: Tue, 4 Nov 2014 20:43:04 +1100 Subject: [PATCH 067/303] Experimental, compiles and runs but crashes in some exit scenarios. --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/view/render/mousestate.cpp | 4 +++- apps/opencs/view/world/physicsmanager.cpp | 3 +++ apps/opencs/view/world/physicssystem.cpp | 25 +++++++++-------------- apps/opencs/view/world/physicssystem.hpp | 12 +++-------- 5 files changed, 20 insertions(+), 26 deletions(-) diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index ec6f802cfa..b6c24e1900 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -68,7 +68,7 @@ opencs_units (view/world opencs_units_noqt (view/world subviews enumdelegate vartypedelegate recordstatusdelegate idtypedelegate datadisplaydelegate - scripthighlighter idvalidator dialoguecreator physicssystem physicsmanager + scripthighlighter idvalidator dialoguecreator physicssystem physicsmanager physicsengine ) opencs_units (view/widget diff --git a/apps/opencs/view/render/mousestate.cpp b/apps/opencs/view/render/mousestate.cpp index af835d0a54..94ab9bc0bd 100644 --- a/apps/opencs/view/render/mousestate.cpp +++ b/apps/opencs/view/render/mousestate.cpp @@ -266,7 +266,9 @@ namespace CSVRender void MouseState::mouseDoubleClickEvent (QMouseEvent *event) { - event->ignore(); + //event->ignore(); + mPhysics->toggleDebugRendering(mSceneManager); + mParent->flagAsModified(); } bool MouseState::wheelEvent (QWheelEvent *event) diff --git a/apps/opencs/view/world/physicsmanager.cpp b/apps/opencs/view/world/physicsmanager.cpp index f8e022e5df..2d8dcd95af 100644 --- a/apps/opencs/view/world/physicsmanager.cpp +++ b/apps/opencs/view/world/physicsmanager.cpp @@ -74,6 +74,9 @@ namespace CSVWorld throw std::runtime_error("No physics system found for the given document."); } + // deprecated by removeDocument() and may be deleted in future code updates + // however there may be some value in removing the deleted scene widgets from the + // list so that the list does not grow forever void PhysicsManager::removeSceneWidget(CSVRender::WorldspaceWidget *widget) { CSVRender::SceneWidget *sceneWidget = static_cast(widget); diff --git a/apps/opencs/view/world/physicssystem.cpp b/apps/opencs/view/world/physicssystem.cpp index 73063b6bee..98e3a71e99 100644 --- a/apps/opencs/view/world/physicssystem.cpp +++ b/apps/opencs/view/world/physicssystem.cpp @@ -6,7 +6,7 @@ #include #include -#include +#include "physicsengine.hpp" #include #include "../../model/settings/usersettings.hpp" #include "../render/elements.hpp" @@ -15,17 +15,12 @@ namespace CSVWorld { PhysicsSystem::PhysicsSystem() { - // Create physics. shapeLoader is deleted by the physic engine - NifBullet::ManualBulletShapeLoader* shapeLoader = new NifBullet::ManualBulletShapeLoader(); - mEngine = new OEngine::Physic::PhysicEngine(shapeLoader); + mEngine = new PhysicsEngine(); } PhysicsSystem::~PhysicsSystem() { - // FIXME: OEngine does not behave well when multiple instances are created - // and deleted, sometimes resulting in crashes. Skip the deletion until the physics - // code is moved out of OEngine. - //delete mEngine; + delete mEngine; } // looks up the scene manager based on the scene node name (inefficient) @@ -56,8 +51,6 @@ namespace CSVWorld { mEngine->createAndAdjustRigidBody(mesh, referenceId, scale, position, rotation, - 0, // scaledBoxTranslation - 0, // boxRotation true, // raycasting placeable); } @@ -146,7 +139,7 @@ namespace CSVWorld // create a new physics object mEngine->createAndAdjustRigidBody(mesh, referenceId, scale, position, rotation, - 0, 0, true, placeable); + true, placeable); // update other scene managers if they have the referenceId // FIXME: rotation or scale not updated @@ -278,6 +271,8 @@ namespace CSVWorld void PhysicsSystem::addSceneManager(Ogre::SceneManager *sceneMgr, CSVRender::SceneWidget *sceneWidget) { mSceneWidgets[sceneMgr] = sceneWidget; + + mEngine->createDebugDraw(sceneMgr); } std::map PhysicsSystem::sceneWidgets() @@ -287,6 +282,8 @@ namespace CSVWorld void PhysicsSystem::removeSceneManager(Ogre::SceneManager *sceneMgr) { + mEngine->removeDebugDraw(sceneMgr); + mSceneWidgets.erase(sceneMgr); } @@ -310,8 +307,6 @@ namespace CSVWorld if(!sceneMgr) return; - mEngine->setSceneManager(sceneMgr); - CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance(); if(!(userSettings.setting("debug/mouse-picking", QString("false")) == "true" ? true : false)) { @@ -319,7 +314,7 @@ namespace CSVWorld return; } - mEngine->toggleDebugRendering(); - mEngine->stepSimulation(0.0167); // DebugDrawer::step() not directly accessible + mEngine->toggleDebugRendering(sceneMgr); + mEngine->stepDebug(sceneMgr); } } diff --git a/apps/opencs/view/world/physicssystem.hpp b/apps/opencs/view/world/physicssystem.hpp index 0036bf769d..731e527b1d 100644 --- a/apps/opencs/view/world/physicssystem.hpp +++ b/apps/opencs/view/world/physicssystem.hpp @@ -12,14 +12,6 @@ namespace Ogre class Camera; } -namespace OEngine -{ - namespace Physic - { - class PhysicEngine; - } -} - namespace CSVRender { class SceneWidget; @@ -27,13 +19,15 @@ namespace CSVRender namespace CSVWorld { + class PhysicsEngine; + class PhysicsSystem { std::map mSceneNodeToRefId; std::map > mRefIdToSceneNode; std::map mSceneNodeToMesh; std::map mSceneWidgets; - OEngine::Physic::PhysicEngine* mEngine; + PhysicsEngine* mEngine; std::multimap mTerrain; public: From 70b5d6857a182de419fc2faf63a0f7e177da8f7f Mon Sep 17 00:00:00 2001 From: cc9cii Date: Tue, 4 Nov 2014 20:52:28 +1100 Subject: [PATCH 068/303] Add missing files. --- apps/opencs/view/world/physicsengine.cpp | 441 +++++++++++++++++++++++ apps/opencs/view/world/physicsengine.hpp | 253 +++++++++++++ 2 files changed, 694 insertions(+) create mode 100644 apps/opencs/view/world/physicsengine.cpp create mode 100644 apps/opencs/view/world/physicsengine.hpp diff --git a/apps/opencs/view/world/physicsengine.cpp b/apps/opencs/view/world/physicsengine.cpp new file mode 100644 index 0000000000..cdfc818a57 --- /dev/null +++ b/apps/opencs/view/world/physicsengine.cpp @@ -0,0 +1,441 @@ +#include "physicsengine.hpp" + +#include +#include + +#include +#include +#include + +#include + +#include +#include + +// PLEASE NOTE: +// +// This file is based on libs/openengine/bullet/physic.cpp. The commit history and +// credits for the code below stem from that file. + +namespace CSVWorld +{ + RigidBody::RigidBody(btRigidBody::btRigidBodyConstructionInfo &CI, + const std::string &referenceId) : btRigidBody(CI) , mReferenceId(referenceId) + { + } + + RigidBody::~RigidBody() + { + delete getMotionState(); + } + + // In OpenCS one document has one PhysicsSystem. Each PhysicsSystem contains one + // PhysicsEngine. One document can have 0..n SceneWidgets, each with its own + // Ogre::SceneManager. + // + // These relationships are managed by the PhysicsManager. + // + // - When a view is created its document is registered with the PhysicsManager in + // View's constructor. If the document is new a PhysicSystem is created and + // associated with that document. A null list of SceneWidgets are assigned to + // that document. + // + // - When all views for a given document is closed, ViewManager will notify the + // PhysicsManager to destroy the PhysicsSystem associated with the document. + // + // - Each time a WorldspaceWidget (or its subclass) is created, it gets the + // PhysicsSystem associates with the widget's document from the PhysicsManager. + // The list of widgets are then maintained, but is not necessary and may be + // removed in future code updates. + // + // Each WorldspaceWidget can have objects (References) and terrain (Land) loaded + // from its document. There may be several views of the object, however there + // is only one corresponding physics object. i.e. each Reference can have 1..n + // SceneNodes + // + // These relationships are managed by the PhysicsSystem for the document. + // + // - Each time a WorldspaceWidget (or its subclass) is created, it registers + // itself and its Ogre::SceneManager with the PhysicsSystem assigned by the + // PhysicsManager. + // + // - Each time an object is added, the object's Ogre::SceneNode name is registered + // with the PhysicsSystem. Ogre itself maintains which SceneNode belongs to + // which SceneManager. + // + // - Each time an terrain is added, its cell coordinates and the SceneManager is + // registered with the PhysicsSystem. + // + PhysicsEngine::PhysicsEngine() + { + // Set up the collision configuration and dispatcher + mCollisionConfiguration = new btDefaultCollisionConfiguration(); + mDispatcher = new btCollisionDispatcher(mCollisionConfiguration); + + // The actual physics solver + mSolver = new btSequentialImpulseConstraintSolver; + + mBroadphase = new btDbvtBroadphase(); + + // The world. + mDynamicsWorld = new btDiscreteDynamicsWorld(mDispatcher, + mBroadphase, mSolver, mCollisionConfiguration); + + // 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. + mDynamicsWorld->setForceUpdateAllAabbs(false); + + mDynamicsWorld->setGravity(btVector3(0, 0, -10)); + + if(OEngine::Physic::BulletShapeManager::getSingletonPtr() == NULL) + { + new OEngine::Physic::BulletShapeManager(); + } + + mShapeLoader = new NifBullet::ManualBulletShapeLoader(); + } + + PhysicsEngine::~PhysicsEngine() + { + HeightFieldContainer::iterator hf_it = mHeightFieldMap.begin(); + for(; hf_it != mHeightFieldMap.end(); ++hf_it) + { + mDynamicsWorld->removeRigidBody(hf_it->second.mBody); + delete hf_it->second.mShape; + delete hf_it->second.mBody; + } + + RigidBodyContainer::iterator rb_it = mCollisionObjectMap.begin(); + for(; rb_it != mCollisionObjectMap.end(); ++rb_it) + { + if (rb_it->second != NULL) + { + mDynamicsWorld->removeRigidBody(rb_it->second); + + delete rb_it->second; + rb_it->second = NULL; + } + } + + rb_it = mRaycastingObjectMap.begin(); + for (; rb_it != mRaycastingObjectMap.end(); ++rb_it) + { + if (rb_it->second != NULL) + { + mDynamicsWorld->removeRigidBody(rb_it->second); + + delete rb_it->second; + rb_it->second = NULL; + } + } + + delete mDynamicsWorld; // FIXME: need to reference count?? + delete mSolver; + delete mCollisionConfiguration; + delete mDispatcher; + delete mBroadphase; + delete mShapeLoader; + + delete OEngine::Physic::BulletShapeManager::getSingletonPtr(); // FIXME: need to reference count + } + + int PhysicsEngine::toggleDebugRendering(Ogre::SceneManager *sceneMgr) + { + if(!sceneMgr) + return 0; + + std::map::iterator iter = + mDebugDrawers.find(sceneMgr); + if(iter != mDebugDrawers.end()) // found scene manager + { + if((*iter).second) + { + // set a new drawer each time (maybe with a different scene manager) + mDynamicsWorld->setDebugDrawer(mDebugDrawers[sceneMgr]); + if(!mDebugDrawers[sceneMgr]->getDebugMode()) + mDebugDrawers[sceneMgr]->setDebugMode(1 /*mDebugDrawFlags*/); + else + mDebugDrawers[sceneMgr]->setDebugMode(0); + + mDynamicsWorld->debugDrawWorld(); // FIXME: call this now? + return mDebugDrawers[sceneMgr]->getDebugMode(); + } + } + return 0; + } + + void PhysicsEngine::stepDebug(Ogre::SceneManager *sceneMgr) + { + if(!sceneMgr) + return; + + std::map::iterator iter = + mDebugDrawers.find(sceneMgr); + if(iter != mDebugDrawers.end()) // found scene manager + { + if((*iter).second) + (*iter).second->step(); + else + return; + } + } + + void PhysicsEngine::createDebugDraw(Ogre::SceneManager *sceneMgr) + { + if(mDebugDrawers.find(sceneMgr) == mDebugDrawers.end()) + { + mDebugSceneNodes[sceneMgr] = sceneMgr->getRootSceneNode()->createChildSceneNode(); + mDebugDrawers[sceneMgr] = new BtOgre::DebugDrawer(mDebugSceneNodes[sceneMgr], mDynamicsWorld); + mDebugDrawers[sceneMgr]->setDebugMode(0); + } + } + + void PhysicsEngine::removeDebugDraw(Ogre::SceneManager *sceneMgr) + { + std::map::iterator iter = + mDebugDrawers.find(sceneMgr); + if(iter != mDebugDrawers.end()) + { + delete (*iter).second; + mDebugDrawers.erase(iter); + } + + std::map::iterator it = + mDebugSceneNodes.find(sceneMgr); + if(it != mDebugSceneNodes.end()) + { + std::string sceneNodeName = (*it).second->getName(); + if(sceneMgr->hasSceneNode(sceneNodeName)) + sceneMgr->destroySceneNode(sceneNodeName); + } + } + + void PhysicsEngine::addHeightField(float* heights, + int x, int y, float yoffset, + float triSize, float sqrtVerts) + { + const std::string name = "HeightField_" + + boost::lexical_cast(x) + "_" + + boost::lexical_cast(y); + + // find the minimum and maximum heights (needed for bullet) + float minh = heights[0]; + float maxh = heights[0]; + for (int i=0; imaxh) maxh = h; + if (hsetUseDiamondSubdivision(true); + + btVector3 scl(triSize, triSize, 1); + hfShape->setLocalScaling(scl); + + btRigidBody::btRigidBodyConstructionInfo CI = + btRigidBody::btRigidBodyConstructionInfo(0,0,hfShape); + RigidBody* body = new RigidBody(CI, name); + body->getWorldTransform().setOrigin(btVector3( + (x+0.5)*triSize*(sqrtVerts-1), + (y+0.5)*triSize*(sqrtVerts-1), + (maxh+minh)/2.f)); + + HeightField hf; + hf.mBody = body; + hf.mShape = hfShape; + + mHeightFieldMap [name] = hf; + + mDynamicsWorld->addRigidBody(body, CollisionType_HeightMap, + CollisionType_Actor|CollisionType_Raycasting|CollisionType_Projectile); + } + + void PhysicsEngine::removeHeightField(int x, int y) + { + const std::string name = "HeightField_" + + boost::lexical_cast(x) + "_" + + boost::lexical_cast(y); + + HeightField hf = mHeightFieldMap [name]; + + mDynamicsWorld->removeRigidBody(hf.mBody); + delete hf.mShape; + delete hf.mBody; + + mHeightFieldMap.erase(name); + } + + void PhysicsEngine::adjustRigidBody(RigidBody* body, + const Ogre::Vector3 &position, const Ogre::Quaternion &rotation) + { + btTransform tr; + //Ogre::Quaternion boxrot = rotation * boxRotation; + Ogre::Quaternion boxrot = rotation * Ogre::Quaternion::IDENTITY; + //Ogre::Vector3 transrot = boxrot * scaledBoxTranslation; + Ogre::Vector3 transrot = boxrot * Ogre::Vector3::ZERO; + Ogre::Vector3 newPosition = transrot + position; + + tr.setOrigin(btVector3(newPosition.x, newPosition.y, newPosition.z)); + tr.setRotation(btQuaternion(boxrot.x,boxrot.y,boxrot.z,boxrot.w)); + body->setWorldTransform(tr); + } + + RigidBody* PhysicsEngine::createAndAdjustRigidBody(const std::string &mesh, + const std::string &name, // referenceId, assumed unique per OpenCS document + float scale, + const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation, + bool raycasting, + bool placeable) // indicates whether the object can be picked up by the player + { + std::string sid = (boost::format("%07.3f") % scale).str(); + std::string outputstring = mesh + sid; + + //get the shape from the .nif + mShapeLoader->load(outputstring, "General"); + OEngine::Physic::BulletShapeManager::getSingletonPtr()->load(outputstring, "General"); + OEngine::Physic::BulletShapePtr shape = + OEngine::Physic::BulletShapeManager::getSingleton().getByName(outputstring, "General"); + + if (placeable && !raycasting && shape->mCollisionShape && !shape->mHasCollisionNode) + return NULL; + + if (!shape->mCollisionShape && !raycasting) + return NULL; + + if (!shape->mRaycastingShape && raycasting) + return NULL; + + btCollisionShape* collisionShape = raycasting ? shape->mRaycastingShape : shape->mCollisionShape; + collisionShape->setLocalScaling(btVector3(scale, scale, scale)); + + //create the real body + btRigidBody::btRigidBodyConstructionInfo CI = + btRigidBody::btRigidBodyConstructionInfo(0, 0, collisionShape); + + RigidBody* body = new RigidBody(CI, name); + + adjustRigidBody(body, position, rotation); + + if (!raycasting) + { + assert (mCollisionObjectMap.find(name) == mCollisionObjectMap.end()); + mCollisionObjectMap[name] = body; + mDynamicsWorld->addRigidBody(body, + CollisionType_World, CollisionType_Actor|CollisionType_HeightMap); + } + else + { + assert (mRaycastingObjectMap.find(name) == mRaycastingObjectMap.end()); + mRaycastingObjectMap[name] = body; + mDynamicsWorld->addRigidBody(body, + CollisionType_Raycasting, CollisionType_Raycasting|CollisionType_Projectile); + } + + return body; + } + + void PhysicsEngine::removeRigidBody(const std::string &name) + { + RigidBodyContainer::iterator it = mCollisionObjectMap.find(name); + if (it != mCollisionObjectMap.end() ) + { + RigidBody* body = it->second; + if(body != NULL) + { + mDynamicsWorld->removeRigidBody(body); + } + } + + it = mRaycastingObjectMap.find(name); + if (it != mRaycastingObjectMap.end() ) + { + RigidBody* body = it->second; + if(body != NULL) + { + mDynamicsWorld->removeRigidBody(body); + } + } + } + + void PhysicsEngine::deleteRigidBody(const std::string &name) + { + RigidBodyContainer::iterator it = mCollisionObjectMap.find(name); + if (it != mCollisionObjectMap.end() ) + { + RigidBody* body = it->second; + + if(body != NULL) + { + delete body; + } + mCollisionObjectMap.erase(it); + } + + it = mRaycastingObjectMap.find(name); + if (it != mRaycastingObjectMap.end() ) + { + RigidBody* body = it->second; + + if(body != NULL) + { + delete body; + } + mRaycastingObjectMap.erase(it); + } + } + + RigidBody* PhysicsEngine::getRigidBody(const std::string &name, bool raycasting) + { + RigidBodyContainer* map = raycasting ? &mRaycastingObjectMap : &mCollisionObjectMap; + RigidBodyContainer::iterator it = map->find(name); + if (it != map->end() ) + { + RigidBody* body = (*map)[name]; + return body; + } + else + { + return NULL; + } + } + + std::pair PhysicsEngine::rayTest(const btVector3 &from, + const btVector3 &to, bool raycastingObjectOnly, bool ignoreHeightMap, Ogre::Vector3* normal) + { + std::string referenceId = ""; + float d = -1; + + btCollisionWorld::ClosestRayResultCallback resultCallback1(from, to); + resultCallback1.m_collisionFilterGroup = 0xff; + + if(raycastingObjectOnly) + resultCallback1.m_collisionFilterMask = CollisionType_Raycasting|CollisionType_Actor; + else + resultCallback1.m_collisionFilterMask = CollisionType_World; + + if(!ignoreHeightMap) + resultCallback1.m_collisionFilterMask = resultCallback1.m_collisionFilterMask | CollisionType_HeightMap; + + mDynamicsWorld->rayTest(from, to, resultCallback1); + if (resultCallback1.hasHit()) + { + referenceId = static_cast(*resultCallback1.m_collisionObject).getReferenceId(); + d = resultCallback1.m_closestHitFraction; + if (normal) + *normal = Ogre::Vector3(resultCallback1.m_hitNormalWorld.x(), + resultCallback1.m_hitNormalWorld.y(), + resultCallback1.m_hitNormalWorld.z()); + } + + return std::pair(referenceId, d); + } +} diff --git a/apps/opencs/view/world/physicsengine.hpp b/apps/opencs/view/world/physicsengine.hpp new file mode 100644 index 0000000000..9d60fe5f1c --- /dev/null +++ b/apps/opencs/view/world/physicsengine.hpp @@ -0,0 +1,253 @@ +#ifndef CSV_WORLD_PHYSICSENGINE_H +#define CSV_WORLD_PHYSICSENGINE_H + +//#include +#include + +#include +//#include "BulletCollision/CollisionDispatch/btGhostObject.h" +//#include +//#include "BulletCollision/CollisionShapes/btScaledBvhTriangleMeshShape.h" +//#include +#include +#include // needs Ogre::SceneNode defined + +//#include +//#include + +//class btRigidBody; +class btBroadphaseInterface; +class btDefaultCollisionConfiguration; +class btSequentialImpulseConstraintSolver; +class btCollisionDispatcher; +class btDiscreteDynamicsWorld; +class btHeightfieldTerrainShape; +//class btCollisionObject; + +namespace BtOgre +{ + class DebugDrawer; +} + +namespace Ogre +{ + class Vector3; + class SceneNode; + class SceneManager; + class Quaternion; +} + +namespace OEngine +{ + namespace Physic + { + class BulletShapeLoader; + } +} + +namespace CSVWorld +{ + + // enum btIDebugDraw::DebugDrawModes + // { + // DBG_NoDebug=0, + // DBG_DrawWireframe = 1, + // DBG_DrawAabb=2, + // DBG_DrawFeaturesText=4, + // DBG_DrawContactPoints=8, + // DBG_NoDeactivation=16, + // DBG_NoHelpText = 32, + // DBG_DrawText=64, + // DBG_ProfileTimings = 128, + // DBG_EnableSatComparison = 256, + // DBG_DisableBulletLCP = 512, + // DBG_EnableCCD = 1024, + // DBG_DrawConstraints = (1 << 11), + // DBG_DrawConstraintLimits = (1 << 12), + // DBG_FastWireframe = (1<<13), + // DBG_DrawNormals = (1<<14), + // DBG_MAX_DEBUG_DRAW_MODE + // }; + // +#if 0 + class CSVDebugDrawer : public BtOgre::DebugDrawer + { + BtOgre::DebugDrawer *mDebugDrawer; + Ogre::SceneManager *mSceneMgr; + Ogre::SceneNode *mSceneNode; + int mDebugMode; + + public: + + CSVDebugDrawer(Ogre::SceneManager *sceneMgr, btDiscreteDynamicsWorld *dynamicsWorld); + ~CSVDebugDrawer(); + + void setDebugMode(int mode); + bool toggleDebugRendering(); + }; +#endif + + // This class is just an extension of normal btRigidBody in order to add extra info. + // When bullet give back a btRigidBody, you can just do a static_cast to RigidBody, + // so one never should use btRigidBody directly! + class RigidBody: public btRigidBody + { + std::string mReferenceId; + + public: + + RigidBody(btRigidBody::btRigidBodyConstructionInfo &CI, const std::string &referenceId); + virtual ~RigidBody(); + + std::string getReferenceId() const { return mReferenceId; } + }; + + struct HeightField + { + btHeightfieldTerrainShape* mShape; + RigidBody* mBody; + }; + + /** + * The PhysicsEngine class contain everything which is needed for Physic. + * It's needed that Ogre Resources are set up before the PhysicsEngine is created. + * Note:deleting it WILL NOT delete the RigidBody! + * TODO:unload unused resources? + */ + class PhysicsEngine + { + //Bullet Stuff + btBroadphaseInterface *mBroadphase; + btDefaultCollisionConfiguration *mCollisionConfiguration; + btSequentialImpulseConstraintSolver *mSolver; + btCollisionDispatcher *mDispatcher; + btDiscreteDynamicsWorld *mDynamicsWorld; + + //the NIF file loader. + OEngine::Physic::BulletShapeLoader *mShapeLoader; + + typedef std::map HeightFieldContainer; + HeightFieldContainer mHeightFieldMap; + + typedef std::map RigidBodyContainer; + RigidBodyContainer mCollisionObjectMap; + + RigidBodyContainer mRaycastingObjectMap; + + std::map mDebugDrawers; + std::map mDebugSceneNodes; + +#if 0 + // from bullet + enum CollisionFilterGroups + { + DefaultFilter = 1, + StaticFilter = 2, + KinematicFilter = 4, + DebrisFilter = 8, + SensorTrigger = 16, + CharacterFilter = 32, + AllFilter = -1 //all bits sets: DefaultFilter | StaticFilter | KinematicFilter | DebrisFilter | SensorTrigger + }; +#endif + + enum CollisionType { + CollisionType_Nothing = 0, // rayTest(const btVector3 &from, + const btVector3 &to, bool raycastingObjectOnly = true, + bool ignoreHeightMap = false, Ogre::Vector3* normal = NULL); + + private: + + PhysicsEngine(const PhysicsEngine&); + PhysicsEngine& operator=(const PhysicsEngine&); + + // Create a debug rendering. It is called by setDebgRenderingMode if it's + // not created yet. + // Important Note: this will crash if the Render is not yet initialised! + void createDebugRendering(); + + // Set the debug rendering mode. 0 to turn it off. + // Important Note: this will crash if the Render is not yet initialised! + void setDebugRenderingMode(int mode); +#if 0 + void getObjectAABB(const std::string &mesh, + float scale, btVector3 &min, btVector3 &max); + + /** + * Return all objects hit by a ray. + */ + std::vector< std::pair > rayTest2(const btVector3 &from, const btVector3 &to, int filterGroup=0xff); + + std::pair sphereCast (float radius, btVector3 &from, btVector3 &to); + ///< @return (hit, relative distance) + + std::vector getCollisions(const std::string &name, int collisionGroup, int collisionMask); + + // Get the nearest object that's inside the given object, filtering out objects of the + // provided name + std::pair getFilteredContact(const std::string &filter, + const btVector3 &origin, + btCollisionObject *object); +#endif + }; +} +#endif // CSV_WORLD_PHYSICSENGINE_H From 37a050873c2aa289a96206dda95657a004effa6b Mon Sep 17 00:00:00 2001 From: cc9cii Date: Tue, 4 Nov 2014 21:46:35 +1100 Subject: [PATCH 069/303] Fix crash when multiple scenewidgets were closed. BtOgre was destroying resources each time. --- apps/opencs/view/world/physicsengine.cpp | 13 +++++++++++++ apps/opencs/view/world/physicsengine.hpp | 2 -- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/opencs/view/world/physicsengine.cpp b/apps/opencs/view/world/physicsengine.cpp index cdfc818a57..7e7078a0f9 100644 --- a/apps/opencs/view/world/physicsengine.cpp +++ b/apps/opencs/view/world/physicsengine.cpp @@ -11,6 +11,8 @@ #include #include +#include +#include // needs Ogre::SceneNode defined // PLEASE NOTE: // @@ -199,6 +201,17 @@ namespace CSVWorld { delete (*iter).second; mDebugDrawers.erase(iter); + + // BtOgre::DebugDrawer destroys the resources leading to crashes in some + // situations. Workaround by recreating them each time. + if (!Ogre::ResourceGroupManager::getSingleton().resourceGroupExists("BtOgre")) + Ogre::ResourceGroupManager::getSingleton().createResourceGroup("BtOgre"); + if (!Ogre::MaterialManager::getSingleton().resourceExists("BtOgre/DebugLines")) + { + Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().create("BtOgre/DebugLines", "BtOgre"); + mat->setReceiveShadows(false); + mat->setSelfIllumination(1,1,1); + } } std::map::iterator it = diff --git a/apps/opencs/view/world/physicsengine.hpp b/apps/opencs/view/world/physicsengine.hpp index 9d60fe5f1c..b14d358205 100644 --- a/apps/opencs/view/world/physicsengine.hpp +++ b/apps/opencs/view/world/physicsengine.hpp @@ -9,8 +9,6 @@ //#include //#include "BulletCollision/CollisionShapes/btScaledBvhTriangleMeshShape.h" //#include -#include -#include // needs Ogre::SceneNode defined //#include //#include From 7d133d508d4ec7db1cf33bcd64bb65f4159d4c4c Mon Sep 17 00:00:00 2001 From: cc9cii Date: Wed, 5 Nov 2014 07:10:14 +1100 Subject: [PATCH 070/303] Remove unused code. Move the cleanup of global resources used by the PhysicsEngine to PhysicsManager. --- apps/opencs/view/render/mousestate.cpp | 8 +- apps/opencs/view/world/physicsengine.cpp | 16 ++-- apps/opencs/view/world/physicsengine.hpp | 101 +--------------------- apps/opencs/view/world/physicsmanager.cpp | 15 ++++ 4 files changed, 32 insertions(+), 108 deletions(-) diff --git a/apps/opencs/view/render/mousestate.cpp b/apps/opencs/view/render/mousestate.cpp index 94ab9bc0bd..45e846f74a 100644 --- a/apps/opencs/view/render/mousestate.cpp +++ b/apps/opencs/view/render/mousestate.cpp @@ -255,7 +255,7 @@ namespace CSVRender std::pair result = terrainUnderCursor(event->x(), event->y()); if(result.first != "") { - // FIXME: terrain editing + // FIXME: terrain editing goes here } break; } @@ -266,9 +266,9 @@ namespace CSVRender void MouseState::mouseDoubleClickEvent (QMouseEvent *event) { - //event->ignore(); - mPhysics->toggleDebugRendering(mSceneManager); - mParent->flagAsModified(); + event->ignore(); + //mPhysics->toggleDebugRendering(mSceneManager); + //mParent->flagAsModified(); } bool MouseState::wheelEvent (QWheelEvent *event) diff --git a/apps/opencs/view/world/physicsengine.cpp b/apps/opencs/view/world/physicsengine.cpp index 7e7078a0f9..05dcc34265 100644 --- a/apps/opencs/view/world/physicsengine.cpp +++ b/apps/opencs/view/world/physicsengine.cpp @@ -16,8 +16,9 @@ // PLEASE NOTE: // -// This file is based on libs/openengine/bullet/physic.cpp. The commit history and -// credits for the code below stem from that file. +// This file is based on libs/openengine/bullet/physic.cpp. Please see the commit +// history and credits for the code below, which is mostly copied from there and +// adapted for use with OpenCS. namespace CSVWorld { @@ -132,14 +133,17 @@ namespace CSVWorld } } - delete mDynamicsWorld; // FIXME: need to reference count?? + + delete mDynamicsWorld; delete mSolver; delete mCollisionConfiguration; delete mDispatcher; delete mBroadphase; delete mShapeLoader; - delete OEngine::Physic::BulletShapeManager::getSingletonPtr(); // FIXME: need to reference count + // NOTE: the global resources such as "BtOgre/DebugLines" and the + // BulletShapeManager singleton need to be deleted only when all physics + // engines are deleted in PhysicsManager::removeDocument() } int PhysicsEngine::toggleDebugRendering(Ogre::SceneManager *sceneMgr) @@ -159,8 +163,8 @@ namespace CSVWorld mDebugDrawers[sceneMgr]->setDebugMode(1 /*mDebugDrawFlags*/); else mDebugDrawers[sceneMgr]->setDebugMode(0); + mDynamicsWorld->debugDrawWorld(); - mDynamicsWorld->debugDrawWorld(); // FIXME: call this now? return mDebugDrawers[sceneMgr]->getDebugMode(); } } @@ -290,9 +294,7 @@ namespace CSVWorld const Ogre::Vector3 &position, const Ogre::Quaternion &rotation) { btTransform tr; - //Ogre::Quaternion boxrot = rotation * boxRotation; Ogre::Quaternion boxrot = rotation * Ogre::Quaternion::IDENTITY; - //Ogre::Vector3 transrot = boxrot * scaledBoxTranslation; Ogre::Vector3 transrot = boxrot * Ogre::Vector3::ZERO; Ogre::Vector3 newPosition = transrot + position; diff --git a/apps/opencs/view/world/physicsengine.hpp b/apps/opencs/view/world/physicsengine.hpp index b14d358205..d960abdff7 100644 --- a/apps/opencs/view/world/physicsengine.hpp +++ b/apps/opencs/view/world/physicsengine.hpp @@ -1,26 +1,16 @@ #ifndef CSV_WORLD_PHYSICSENGINE_H #define CSV_WORLD_PHYSICSENGINE_H -//#include #include #include -//#include "BulletCollision/CollisionDispatch/btGhostObject.h" -//#include -//#include "BulletCollision/CollisionShapes/btScaledBvhTriangleMeshShape.h" -//#include -//#include -//#include - -//class btRigidBody; class btBroadphaseInterface; class btDefaultCollisionConfiguration; class btSequentialImpulseConstraintSolver; class btCollisionDispatcher; class btDiscreteDynamicsWorld; class btHeightfieldTerrainShape; -//class btCollisionObject; namespace BtOgre { @@ -45,46 +35,6 @@ namespace OEngine namespace CSVWorld { - - // enum btIDebugDraw::DebugDrawModes - // { - // DBG_NoDebug=0, - // DBG_DrawWireframe = 1, - // DBG_DrawAabb=2, - // DBG_DrawFeaturesText=4, - // DBG_DrawContactPoints=8, - // DBG_NoDeactivation=16, - // DBG_NoHelpText = 32, - // DBG_DrawText=64, - // DBG_ProfileTimings = 128, - // DBG_EnableSatComparison = 256, - // DBG_DisableBulletLCP = 512, - // DBG_EnableCCD = 1024, - // DBG_DrawConstraints = (1 << 11), - // DBG_DrawConstraintLimits = (1 << 12), - // DBG_FastWireframe = (1<<13), - // DBG_DrawNormals = (1<<14), - // DBG_MAX_DEBUG_DRAW_MODE - // }; - // -#if 0 - class CSVDebugDrawer : public BtOgre::DebugDrawer - { - BtOgre::DebugDrawer *mDebugDrawer; - Ogre::SceneManager *mSceneMgr; - Ogre::SceneNode *mSceneNode; - int mDebugMode; - - public: - - CSVDebugDrawer(Ogre::SceneManager *sceneMgr, btDiscreteDynamicsWorld *dynamicsWorld); - ~CSVDebugDrawer(); - - void setDebugMode(int mode); - bool toggleDebugRendering(); - }; -#endif - // This class is just an extension of normal btRigidBody in order to add extra info. // When bullet give back a btRigidBody, you can just do a static_cast to RigidBody, // so one never should use btRigidBody directly! @@ -106,12 +56,9 @@ namespace CSVWorld RigidBody* mBody; }; - /** - * The PhysicsEngine class contain everything which is needed for Physic. - * It's needed that Ogre Resources are set up before the PhysicsEngine is created. - * Note:deleting it WILL NOT delete the RigidBody! - * TODO:unload unused resources? - */ + // The PhysicsEngine class contain everything which is needed for Physic. + // It's needed that Ogre Resources are set up before the PhysicsEngine is created. + // Note:deleting it WILL NOT delete the RigidBody! class PhysicsEngine { //Bullet Stuff @@ -135,20 +82,6 @@ namespace CSVWorld std::map mDebugDrawers; std::map mDebugSceneNodes; -#if 0 - // from bullet - enum CollisionFilterGroups - { - DefaultFilter = 1, - StaticFilter = 2, - KinematicFilter = 4, - DebrisFilter = 8, - SensorTrigger = 16, - CharacterFilter = 32, - AllFilter = -1 //all bits sets: DefaultFilter | StaticFilter | KinematicFilter | DebrisFilter | SensorTrigger - }; -#endif - enum CollisionType { CollisionType_Nothing = 0, // > rayTest2(const btVector3 &from, const btVector3 &to, int filterGroup=0xff); - - std::pair sphereCast (float radius, btVector3 &from, btVector3 &to); - ///< @return (hit, relative distance) - - std::vector getCollisions(const std::string &name, int collisionGroup, int collisionMask); - - // Get the nearest object that's inside the given object, filtering out objects of the - // provided name - std::pair getFilteredContact(const std::string &filter, - const btVector3 &origin, - btCollisionObject *object); -#endif }; } #endif // CSV_WORLD_PHYSICSENGINE_H diff --git a/apps/opencs/view/world/physicsmanager.cpp b/apps/opencs/view/world/physicsmanager.cpp index 2d8dcd95af..ce5748e8d5 100644 --- a/apps/opencs/view/world/physicsmanager.cpp +++ b/apps/opencs/view/world/physicsmanager.cpp @@ -2,6 +2,9 @@ #include +#include +#include + #include "../render/worldspacewidget.hpp" #include "physicssystem.hpp" @@ -54,6 +57,18 @@ namespace CSVWorld { mSceneWidgets.erase(it); } + + // cleanup global resources + if(mPhysics.empty()) + { + // delete the extra resources created in removeDebugDraw + if (Ogre::MaterialManager::getSingleton().resourceExists("BtOgre/DebugLines")) + Ogre::MaterialManager::getSingleton().remove("BtOgre/DebugLines"); + if (Ogre::ResourceGroupManager::getSingleton().resourceGroupExists("BtOgre")) + Ogre::ResourceGroupManager::getSingleton().destroyResourceGroup("BtOgre"); + + delete OEngine::Physic::BulletShapeManager::getSingletonPtr(); + } } // called from CSVRender::WorldspaceWidget() to get widgets' association with Document& From 03abd69b4fed09706c70e34e53c0095f2dc8cb35 Mon Sep 17 00:00:00 2001 From: cc9cii Date: Wed, 5 Nov 2014 07:20:20 +1100 Subject: [PATCH 071/303] Include string header for gcc. --- apps/opencs/view/world/physicsengine.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/opencs/view/world/physicsengine.hpp b/apps/opencs/view/world/physicsengine.hpp index d960abdff7..58dbf65252 100644 --- a/apps/opencs/view/world/physicsengine.hpp +++ b/apps/opencs/view/world/physicsengine.hpp @@ -1,6 +1,7 @@ #ifndef CSV_WORLD_PHYSICSENGINE_H #define CSV_WORLD_PHYSICSENGINE_H +#include #include #include From ba3d2be8e31a52f97f87f9cb5a5a8cb4890513c0 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 5 Nov 2014 19:45:32 +0100 Subject: [PATCH 072/303] Add missing include (Fixes #2108) --- apps/opencs/view/render/cell.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/opencs/view/render/cell.hpp b/apps/opencs/view/render/cell.hpp index e63e095200..67117bfc31 100644 --- a/apps/opencs/view/render/cell.hpp +++ b/apps/opencs/view/render/cell.hpp @@ -3,6 +3,7 @@ #include #include +#include #include From 6b88f5f33edc4abd1e3b6f1c211ad867503c7126 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 5 Nov 2014 20:58:49 +0100 Subject: [PATCH 073/303] Use Ogre::uint8, fixes a missing include for uint8_t --- apps/opencs/view/render/textoverlay.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/opencs/view/render/textoverlay.cpp b/apps/opencs/view/render/textoverlay.cpp index 12831be9a3..656ea959c7 100644 --- a/apps/opencs/view/render/textoverlay.cpp +++ b/apps/opencs/view/render/textoverlay.cpp @@ -79,7 +79,7 @@ TextOverlay::TextOverlay(const Ogre::MovableObject* obj, const Ogre::Camera* cam pixelBuffer->lock(Ogre::HardwareBuffer::HBL_NORMAL); const Ogre::PixelBox& pixelBox = pixelBuffer->getCurrentLock(); - uint8_t* pDest = static_cast(pixelBox.data); + Ogre::uint8* pDest = static_cast(pixelBox.data); // Fill in some pixel data. This will give a semi-transparent blue, // but this is of course dependent on the chosen pixel format. From 5909fa4368aec785e2d09a9e807936ea520d624f Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 5 Nov 2014 21:18:43 +0100 Subject: [PATCH 074/303] new version number and updated changelog --- CMakeLists.txt | 2 +- readme.txt | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4efb19719c..b3ec6cf6d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 33) -set(OPENMW_VERSION_RELEASE 0) +set(OPENMW_VERSION_RELEASE 1) set(OPENMW_VERSION_COMMITHASH "") set(OPENMW_VERSION_TAGHASH "") diff --git a/readme.txt b/readme.txt index cb533e9fbb..810a0e055e 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ OpenMW: A reimplementation of The Elder Scrolls III: Morrowind OpenMW is an attempt at recreating the engine for the popular role-playing game Morrowind by Bethesda Softworks. You need to own and install the original game for OpenMW to work. -Version: 0.33.0 +Version: 0.33.1 License: GPL (see GPL3.txt for more information) Website: http://www.openmw.org @@ -98,6 +98,10 @@ Allowed options: CHANGELOG +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 From f051fb65ff99707f9443fe1feb52b10a752d70ad Mon Sep 17 00:00:00 2001 From: cc9cii Date: Thu, 6 Nov 2014 08:35:24 +1100 Subject: [PATCH 075/303] Fix memory leak when multiple documents in 3D edit. Support multiple physics engine per document. --- apps/opencs/view/world/physicsmanager.cpp | 9 +-- apps/opencs/view/world/physicssystem.cpp | 10 ++- apps/opencs/view/world/physicssystem.hpp | 12 +++- apps/openmw/mwworld/physicssystem.cpp | 2 + libs/openengine/bullet/BtOgreExtras.h | 6 +- libs/openengine/bullet/physic.cpp | 76 ++++++++++++++++++++++- libs/openengine/bullet/physic.hpp | 9 +++ 7 files changed, 106 insertions(+), 18 deletions(-) diff --git a/apps/opencs/view/world/physicsmanager.cpp b/apps/opencs/view/world/physicsmanager.cpp index ce5748e8d5..fa8db9e1e0 100644 --- a/apps/opencs/view/world/physicsmanager.cpp +++ b/apps/opencs/view/world/physicsmanager.cpp @@ -2,7 +2,6 @@ #include -#include #include #include "../render/worldspacewidget.hpp" @@ -58,15 +57,9 @@ namespace CSVWorld mSceneWidgets.erase(it); } - // cleanup global resources + // cleanup global resources used by OEngine if(mPhysics.empty()) { - // delete the extra resources created in removeDebugDraw - if (Ogre::MaterialManager::getSingleton().resourceExists("BtOgre/DebugLines")) - Ogre::MaterialManager::getSingleton().remove("BtOgre/DebugLines"); - if (Ogre::ResourceGroupManager::getSingleton().resourceGroupExists("BtOgre")) - Ogre::ResourceGroupManager::getSingleton().destroyResourceGroup("BtOgre"); - delete OEngine::Physic::BulletShapeManager::getSingletonPtr(); } } diff --git a/apps/opencs/view/world/physicssystem.cpp b/apps/opencs/view/world/physicssystem.cpp index 98e3a71e99..57909f4d38 100644 --- a/apps/opencs/view/world/physicssystem.cpp +++ b/apps/opencs/view/world/physicssystem.cpp @@ -6,7 +6,7 @@ #include #include -#include "physicsengine.hpp" +#include #include #include "../../model/settings/usersettings.hpp" #include "../render/elements.hpp" @@ -15,7 +15,9 @@ namespace CSVWorld { PhysicsSystem::PhysicsSystem() { - mEngine = new PhysicsEngine(); + // Create physics. shapeLoader is deleted by the physic engine + NifBullet::ManualBulletShapeLoader* shapeLoader = new NifBullet::ManualBulletShapeLoader(); + mEngine = new OEngine::Physic::PhysicEngine(shapeLoader); } PhysicsSystem::~PhysicsSystem() @@ -51,6 +53,8 @@ namespace CSVWorld { mEngine->createAndAdjustRigidBody(mesh, referenceId, scale, position, rotation, + 0, // scaledBoxTranslation + 0, // boxRotation true, // raycasting placeable); } @@ -139,7 +143,7 @@ namespace CSVWorld // create a new physics object mEngine->createAndAdjustRigidBody(mesh, referenceId, scale, position, rotation, - true, placeable); + 0, 0, true, placeable); // update other scene managers if they have the referenceId // FIXME: rotation or scale not updated diff --git a/apps/opencs/view/world/physicssystem.hpp b/apps/opencs/view/world/physicssystem.hpp index 731e527b1d..0036bf769d 100644 --- a/apps/opencs/view/world/physicssystem.hpp +++ b/apps/opencs/view/world/physicssystem.hpp @@ -12,6 +12,14 @@ namespace Ogre class Camera; } +namespace OEngine +{ + namespace Physic + { + class PhysicEngine; + } +} + namespace CSVRender { class SceneWidget; @@ -19,15 +27,13 @@ namespace CSVRender namespace CSVWorld { - class PhysicsEngine; - class PhysicsSystem { std::map mSceneNodeToRefId; std::map > mRefIdToSceneNode; std::map mSceneNodeToMesh; std::map mSceneWidgets; - PhysicsEngine* mEngine; + OEngine::Physic::PhysicEngine* mEngine; std::multimap mTerrain; public: diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index aba120baae..80cccd063c 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include @@ -498,6 +499,7 @@ namespace MWWorld if (mWaterCollisionObject.get()) mEngine->mDynamicsWorld->removeCollisionObject(mWaterCollisionObject.get()); delete mEngine; + delete OEngine::Physic::BulletShapeManager::getSingletonPtr(); } OEngine::Physic::PhysicEngine* PhysicsSystem::getEngine() diff --git a/libs/openengine/bullet/BtOgreExtras.h b/libs/openengine/bullet/BtOgreExtras.h index 9572b8a7b7..f8c1fe41d0 100644 --- a/libs/openengine/bullet/BtOgreExtras.h +++ b/libs/openengine/bullet/BtOgreExtras.h @@ -212,8 +212,10 @@ public: ~DebugDrawer() { - Ogre::MaterialManager::getSingleton().remove("BtOgre/DebugLines"); - Ogre::ResourceGroupManager::getSingleton().destroyResourceGroup("BtOgre"); + if (Ogre::MaterialManager::getSingleton().resourceExists("BtOgre/DebugLines")) + Ogre::MaterialManager::getSingleton().remove("BtOgre/DebugLines"); + if (Ogre::ResourceGroupManager::getSingleton().resourceGroupExists("BtOgre")) + Ogre::ResourceGroupManager::getSingleton().destroyResourceGroup("BtOgre"); delete mLineDrawer; } diff --git a/libs/openengine/bullet/physic.cpp b/libs/openengine/bullet/physic.cpp index 0c61253bf0..d7db81595b 100644 --- a/libs/openengine/bullet/physic.cpp +++ b/libs/openengine/bullet/physic.cpp @@ -114,7 +114,7 @@ namespace Physic { mEngine->mDynamicsWorld->removeRigidBody(mBody); delete mBody; - } + } } void PhysicActor::enableCollisionMode(bool collision) @@ -356,7 +356,8 @@ namespace Physic delete broadphase; delete mShapeLoader; - delete BulletShapeManager::getSingletonPtr(); + // Moved the cleanup to mwworld/physicssystem + //delete BulletShapeManager::getSingletonPtr(); } void PhysicEngine::addHeightField(float* heights, @@ -863,5 +864,76 @@ namespace Physic } } + int PhysicEngine::toggleDebugRendering(Ogre::SceneManager *sceneMgr) + { + if(!sceneMgr) + return 0; + + std::map::iterator iter = + mDebugDrawers.find(sceneMgr); + if(iter != mDebugDrawers.end()) // found scene manager + { + if((*iter).second) + { + // set a new drawer each time (maybe with a different scene manager) + mDynamicsWorld->setDebugDrawer(mDebugDrawers[sceneMgr]); + if(!mDebugDrawers[sceneMgr]->getDebugMode()) + mDebugDrawers[sceneMgr]->setDebugMode(1 /*mDebugDrawFlags*/); + else + mDebugDrawers[sceneMgr]->setDebugMode(0); + mDynamicsWorld->debugDrawWorld(); + + return mDebugDrawers[sceneMgr]->getDebugMode(); + } + } + return 0; + } + + void PhysicEngine::stepDebug(Ogre::SceneManager *sceneMgr) + { + if(!sceneMgr) + return; + + std::map::iterator iter = + mDebugDrawers.find(sceneMgr); + if(iter != mDebugDrawers.end()) // found scene manager + { + if((*iter).second) + (*iter).second->step(); + else + return; + } + } + + void PhysicEngine::createDebugDraw(Ogre::SceneManager *sceneMgr) + { + if(mDebugDrawers.find(sceneMgr) == mDebugDrawers.end()) + { + mDebugSceneNodes[sceneMgr] = sceneMgr->getRootSceneNode()->createChildSceneNode(); + mDebugDrawers[sceneMgr] = new BtOgre::DebugDrawer(mDebugSceneNodes[sceneMgr], mDynamicsWorld); + mDebugDrawers[sceneMgr]->setDebugMode(0); + } + } + + void PhysicEngine::removeDebugDraw(Ogre::SceneManager *sceneMgr) + { + std::map::iterator iter = + mDebugDrawers.find(sceneMgr); + if(iter != mDebugDrawers.end()) + { + delete (*iter).second; + mDebugDrawers.erase(iter); + } + + std::map::iterator it = + mDebugSceneNodes.find(sceneMgr); + if(it != mDebugSceneNodes.end()) + { + std::string sceneNodeName = (*it).second->getName(); + if(sceneMgr->hasSceneNode(sceneNodeName)) + sceneMgr->destroySceneNode(sceneNodeName); + } + } + } } diff --git a/libs/openengine/bullet/physic.hpp b/libs/openengine/bullet/physic.hpp index a77b60ab6f..fb6ae0cebe 100644 --- a/libs/openengine/bullet/physic.hpp +++ b/libs/openengine/bullet/physic.hpp @@ -348,6 +348,15 @@ namespace Physic bool isDebugCreated; bool mDebugActive; + // for OpenCS with multiple engines per document + std::map mDebugDrawers; + std::map mDebugSceneNodes; + + int toggleDebugRendering(Ogre::SceneManager *sceneMgr); + void stepDebug(Ogre::SceneManager *sceneMgr); + void createDebugDraw(Ogre::SceneManager *sceneMgr); + void removeDebugDraw(Ogre::SceneManager *sceneMgr); + private: PhysicEngine(const PhysicEngine&); PhysicEngine& operator=(const PhysicEngine&); From a3a06821388493c8466898eb48fb4ade20ff03ed Mon Sep 17 00:00:00 2001 From: cc9cii Date: Thu, 6 Nov 2014 11:16:17 +1100 Subject: [PATCH 076/303] Remove files no longer used, reverting to OEngine. --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/view/world/physicsengine.cpp | 456 ----------------------- apps/opencs/view/world/physicsengine.hpp | 159 -------- 3 files changed, 1 insertion(+), 616 deletions(-) delete mode 100644 apps/opencs/view/world/physicsengine.cpp delete mode 100644 apps/opencs/view/world/physicsengine.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index b6c24e1900..ec6f802cfa 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -68,7 +68,7 @@ opencs_units (view/world opencs_units_noqt (view/world subviews enumdelegate vartypedelegate recordstatusdelegate idtypedelegate datadisplaydelegate - scripthighlighter idvalidator dialoguecreator physicssystem physicsmanager physicsengine + scripthighlighter idvalidator dialoguecreator physicssystem physicsmanager ) opencs_units (view/widget diff --git a/apps/opencs/view/world/physicsengine.cpp b/apps/opencs/view/world/physicsengine.cpp deleted file mode 100644 index 05dcc34265..0000000000 --- a/apps/opencs/view/world/physicsengine.cpp +++ /dev/null @@ -1,456 +0,0 @@ -#include "physicsengine.hpp" - -#include -#include - -#include -#include -#include - -#include - -#include -#include -#include -#include // needs Ogre::SceneNode defined - -// PLEASE NOTE: -// -// This file is based on libs/openengine/bullet/physic.cpp. Please see the commit -// history and credits for the code below, which is mostly copied from there and -// adapted for use with OpenCS. - -namespace CSVWorld -{ - RigidBody::RigidBody(btRigidBody::btRigidBodyConstructionInfo &CI, - const std::string &referenceId) : btRigidBody(CI) , mReferenceId(referenceId) - { - } - - RigidBody::~RigidBody() - { - delete getMotionState(); - } - - // In OpenCS one document has one PhysicsSystem. Each PhysicsSystem contains one - // PhysicsEngine. One document can have 0..n SceneWidgets, each with its own - // Ogre::SceneManager. - // - // These relationships are managed by the PhysicsManager. - // - // - When a view is created its document is registered with the PhysicsManager in - // View's constructor. If the document is new a PhysicSystem is created and - // associated with that document. A null list of SceneWidgets are assigned to - // that document. - // - // - When all views for a given document is closed, ViewManager will notify the - // PhysicsManager to destroy the PhysicsSystem associated with the document. - // - // - Each time a WorldspaceWidget (or its subclass) is created, it gets the - // PhysicsSystem associates with the widget's document from the PhysicsManager. - // The list of widgets are then maintained, but is not necessary and may be - // removed in future code updates. - // - // Each WorldspaceWidget can have objects (References) and terrain (Land) loaded - // from its document. There may be several views of the object, however there - // is only one corresponding physics object. i.e. each Reference can have 1..n - // SceneNodes - // - // These relationships are managed by the PhysicsSystem for the document. - // - // - Each time a WorldspaceWidget (or its subclass) is created, it registers - // itself and its Ogre::SceneManager with the PhysicsSystem assigned by the - // PhysicsManager. - // - // - Each time an object is added, the object's Ogre::SceneNode name is registered - // with the PhysicsSystem. Ogre itself maintains which SceneNode belongs to - // which SceneManager. - // - // - Each time an terrain is added, its cell coordinates and the SceneManager is - // registered with the PhysicsSystem. - // - PhysicsEngine::PhysicsEngine() - { - // Set up the collision configuration and dispatcher - mCollisionConfiguration = new btDefaultCollisionConfiguration(); - mDispatcher = new btCollisionDispatcher(mCollisionConfiguration); - - // The actual physics solver - mSolver = new btSequentialImpulseConstraintSolver; - - mBroadphase = new btDbvtBroadphase(); - - // The world. - mDynamicsWorld = new btDiscreteDynamicsWorld(mDispatcher, - mBroadphase, mSolver, mCollisionConfiguration); - - // 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. - mDynamicsWorld->setForceUpdateAllAabbs(false); - - mDynamicsWorld->setGravity(btVector3(0, 0, -10)); - - if(OEngine::Physic::BulletShapeManager::getSingletonPtr() == NULL) - { - new OEngine::Physic::BulletShapeManager(); - } - - mShapeLoader = new NifBullet::ManualBulletShapeLoader(); - } - - PhysicsEngine::~PhysicsEngine() - { - HeightFieldContainer::iterator hf_it = mHeightFieldMap.begin(); - for(; hf_it != mHeightFieldMap.end(); ++hf_it) - { - mDynamicsWorld->removeRigidBody(hf_it->second.mBody); - delete hf_it->second.mShape; - delete hf_it->second.mBody; - } - - RigidBodyContainer::iterator rb_it = mCollisionObjectMap.begin(); - for(; rb_it != mCollisionObjectMap.end(); ++rb_it) - { - if (rb_it->second != NULL) - { - mDynamicsWorld->removeRigidBody(rb_it->second); - - delete rb_it->second; - rb_it->second = NULL; - } - } - - rb_it = mRaycastingObjectMap.begin(); - for (; rb_it != mRaycastingObjectMap.end(); ++rb_it) - { - if (rb_it->second != NULL) - { - mDynamicsWorld->removeRigidBody(rb_it->second); - - delete rb_it->second; - rb_it->second = NULL; - } - } - - - delete mDynamicsWorld; - delete mSolver; - delete mCollisionConfiguration; - delete mDispatcher; - delete mBroadphase; - delete mShapeLoader; - - // NOTE: the global resources such as "BtOgre/DebugLines" and the - // BulletShapeManager singleton need to be deleted only when all physics - // engines are deleted in PhysicsManager::removeDocument() - } - - int PhysicsEngine::toggleDebugRendering(Ogre::SceneManager *sceneMgr) - { - if(!sceneMgr) - return 0; - - std::map::iterator iter = - mDebugDrawers.find(sceneMgr); - if(iter != mDebugDrawers.end()) // found scene manager - { - if((*iter).second) - { - // set a new drawer each time (maybe with a different scene manager) - mDynamicsWorld->setDebugDrawer(mDebugDrawers[sceneMgr]); - if(!mDebugDrawers[sceneMgr]->getDebugMode()) - mDebugDrawers[sceneMgr]->setDebugMode(1 /*mDebugDrawFlags*/); - else - mDebugDrawers[sceneMgr]->setDebugMode(0); - mDynamicsWorld->debugDrawWorld(); - - return mDebugDrawers[sceneMgr]->getDebugMode(); - } - } - return 0; - } - - void PhysicsEngine::stepDebug(Ogre::SceneManager *sceneMgr) - { - if(!sceneMgr) - return; - - std::map::iterator iter = - mDebugDrawers.find(sceneMgr); - if(iter != mDebugDrawers.end()) // found scene manager - { - if((*iter).second) - (*iter).second->step(); - else - return; - } - } - - void PhysicsEngine::createDebugDraw(Ogre::SceneManager *sceneMgr) - { - if(mDebugDrawers.find(sceneMgr) == mDebugDrawers.end()) - { - mDebugSceneNodes[sceneMgr] = sceneMgr->getRootSceneNode()->createChildSceneNode(); - mDebugDrawers[sceneMgr] = new BtOgre::DebugDrawer(mDebugSceneNodes[sceneMgr], mDynamicsWorld); - mDebugDrawers[sceneMgr]->setDebugMode(0); - } - } - - void PhysicsEngine::removeDebugDraw(Ogre::SceneManager *sceneMgr) - { - std::map::iterator iter = - mDebugDrawers.find(sceneMgr); - if(iter != mDebugDrawers.end()) - { - delete (*iter).second; - mDebugDrawers.erase(iter); - - // BtOgre::DebugDrawer destroys the resources leading to crashes in some - // situations. Workaround by recreating them each time. - if (!Ogre::ResourceGroupManager::getSingleton().resourceGroupExists("BtOgre")) - Ogre::ResourceGroupManager::getSingleton().createResourceGroup("BtOgre"); - if (!Ogre::MaterialManager::getSingleton().resourceExists("BtOgre/DebugLines")) - { - Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().create("BtOgre/DebugLines", "BtOgre"); - mat->setReceiveShadows(false); - mat->setSelfIllumination(1,1,1); - } - } - - std::map::iterator it = - mDebugSceneNodes.find(sceneMgr); - if(it != mDebugSceneNodes.end()) - { - std::string sceneNodeName = (*it).second->getName(); - if(sceneMgr->hasSceneNode(sceneNodeName)) - sceneMgr->destroySceneNode(sceneNodeName); - } - } - - void PhysicsEngine::addHeightField(float* heights, - int x, int y, float yoffset, - float triSize, float sqrtVerts) - { - const std::string name = "HeightField_" - + boost::lexical_cast(x) + "_" - + boost::lexical_cast(y); - - // find the minimum and maximum heights (needed for bullet) - float minh = heights[0]; - float maxh = heights[0]; - for (int i=0; imaxh) maxh = h; - if (hsetUseDiamondSubdivision(true); - - btVector3 scl(triSize, triSize, 1); - hfShape->setLocalScaling(scl); - - btRigidBody::btRigidBodyConstructionInfo CI = - btRigidBody::btRigidBodyConstructionInfo(0,0,hfShape); - RigidBody* body = new RigidBody(CI, name); - body->getWorldTransform().setOrigin(btVector3( - (x+0.5)*triSize*(sqrtVerts-1), - (y+0.5)*triSize*(sqrtVerts-1), - (maxh+minh)/2.f)); - - HeightField hf; - hf.mBody = body; - hf.mShape = hfShape; - - mHeightFieldMap [name] = hf; - - mDynamicsWorld->addRigidBody(body, CollisionType_HeightMap, - CollisionType_Actor|CollisionType_Raycasting|CollisionType_Projectile); - } - - void PhysicsEngine::removeHeightField(int x, int y) - { - const std::string name = "HeightField_" - + boost::lexical_cast(x) + "_" - + boost::lexical_cast(y); - - HeightField hf = mHeightFieldMap [name]; - - mDynamicsWorld->removeRigidBody(hf.mBody); - delete hf.mShape; - delete hf.mBody; - - mHeightFieldMap.erase(name); - } - - void PhysicsEngine::adjustRigidBody(RigidBody* body, - const Ogre::Vector3 &position, const Ogre::Quaternion &rotation) - { - btTransform tr; - Ogre::Quaternion boxrot = rotation * Ogre::Quaternion::IDENTITY; - Ogre::Vector3 transrot = boxrot * Ogre::Vector3::ZERO; - Ogre::Vector3 newPosition = transrot + position; - - tr.setOrigin(btVector3(newPosition.x, newPosition.y, newPosition.z)); - tr.setRotation(btQuaternion(boxrot.x,boxrot.y,boxrot.z,boxrot.w)); - body->setWorldTransform(tr); - } - - RigidBody* PhysicsEngine::createAndAdjustRigidBody(const std::string &mesh, - const std::string &name, // referenceId, assumed unique per OpenCS document - float scale, - const Ogre::Vector3 &position, - const Ogre::Quaternion &rotation, - bool raycasting, - bool placeable) // indicates whether the object can be picked up by the player - { - std::string sid = (boost::format("%07.3f") % scale).str(); - std::string outputstring = mesh + sid; - - //get the shape from the .nif - mShapeLoader->load(outputstring, "General"); - OEngine::Physic::BulletShapeManager::getSingletonPtr()->load(outputstring, "General"); - OEngine::Physic::BulletShapePtr shape = - OEngine::Physic::BulletShapeManager::getSingleton().getByName(outputstring, "General"); - - if (placeable && !raycasting && shape->mCollisionShape && !shape->mHasCollisionNode) - return NULL; - - if (!shape->mCollisionShape && !raycasting) - return NULL; - - if (!shape->mRaycastingShape && raycasting) - return NULL; - - btCollisionShape* collisionShape = raycasting ? shape->mRaycastingShape : shape->mCollisionShape; - collisionShape->setLocalScaling(btVector3(scale, scale, scale)); - - //create the real body - btRigidBody::btRigidBodyConstructionInfo CI = - btRigidBody::btRigidBodyConstructionInfo(0, 0, collisionShape); - - RigidBody* body = new RigidBody(CI, name); - - adjustRigidBody(body, position, rotation); - - if (!raycasting) - { - assert (mCollisionObjectMap.find(name) == mCollisionObjectMap.end()); - mCollisionObjectMap[name] = body; - mDynamicsWorld->addRigidBody(body, - CollisionType_World, CollisionType_Actor|CollisionType_HeightMap); - } - else - { - assert (mRaycastingObjectMap.find(name) == mRaycastingObjectMap.end()); - mRaycastingObjectMap[name] = body; - mDynamicsWorld->addRigidBody(body, - CollisionType_Raycasting, CollisionType_Raycasting|CollisionType_Projectile); - } - - return body; - } - - void PhysicsEngine::removeRigidBody(const std::string &name) - { - RigidBodyContainer::iterator it = mCollisionObjectMap.find(name); - if (it != mCollisionObjectMap.end() ) - { - RigidBody* body = it->second; - if(body != NULL) - { - mDynamicsWorld->removeRigidBody(body); - } - } - - it = mRaycastingObjectMap.find(name); - if (it != mRaycastingObjectMap.end() ) - { - RigidBody* body = it->second; - if(body != NULL) - { - mDynamicsWorld->removeRigidBody(body); - } - } - } - - void PhysicsEngine::deleteRigidBody(const std::string &name) - { - RigidBodyContainer::iterator it = mCollisionObjectMap.find(name); - if (it != mCollisionObjectMap.end() ) - { - RigidBody* body = it->second; - - if(body != NULL) - { - delete body; - } - mCollisionObjectMap.erase(it); - } - - it = mRaycastingObjectMap.find(name); - if (it != mRaycastingObjectMap.end() ) - { - RigidBody* body = it->second; - - if(body != NULL) - { - delete body; - } - mRaycastingObjectMap.erase(it); - } - } - - RigidBody* PhysicsEngine::getRigidBody(const std::string &name, bool raycasting) - { - RigidBodyContainer* map = raycasting ? &mRaycastingObjectMap : &mCollisionObjectMap; - RigidBodyContainer::iterator it = map->find(name); - if (it != map->end() ) - { - RigidBody* body = (*map)[name]; - return body; - } - else - { - return NULL; - } - } - - std::pair PhysicsEngine::rayTest(const btVector3 &from, - const btVector3 &to, bool raycastingObjectOnly, bool ignoreHeightMap, Ogre::Vector3* normal) - { - std::string referenceId = ""; - float d = -1; - - btCollisionWorld::ClosestRayResultCallback resultCallback1(from, to); - resultCallback1.m_collisionFilterGroup = 0xff; - - if(raycastingObjectOnly) - resultCallback1.m_collisionFilterMask = CollisionType_Raycasting|CollisionType_Actor; - else - resultCallback1.m_collisionFilterMask = CollisionType_World; - - if(!ignoreHeightMap) - resultCallback1.m_collisionFilterMask = resultCallback1.m_collisionFilterMask | CollisionType_HeightMap; - - mDynamicsWorld->rayTest(from, to, resultCallback1); - if (resultCallback1.hasHit()) - { - referenceId = static_cast(*resultCallback1.m_collisionObject).getReferenceId(); - d = resultCallback1.m_closestHitFraction; - if (normal) - *normal = Ogre::Vector3(resultCallback1.m_hitNormalWorld.x(), - resultCallback1.m_hitNormalWorld.y(), - resultCallback1.m_hitNormalWorld.z()); - } - - return std::pair(referenceId, d); - } -} diff --git a/apps/opencs/view/world/physicsengine.hpp b/apps/opencs/view/world/physicsengine.hpp deleted file mode 100644 index 58dbf65252..0000000000 --- a/apps/opencs/view/world/physicsengine.hpp +++ /dev/null @@ -1,159 +0,0 @@ -#ifndef CSV_WORLD_PHYSICSENGINE_H -#define CSV_WORLD_PHYSICSENGINE_H - -#include -#include - -#include - -class btBroadphaseInterface; -class btDefaultCollisionConfiguration; -class btSequentialImpulseConstraintSolver; -class btCollisionDispatcher; -class btDiscreteDynamicsWorld; -class btHeightfieldTerrainShape; - -namespace BtOgre -{ - class DebugDrawer; -} - -namespace Ogre -{ - class Vector3; - class SceneNode; - class SceneManager; - class Quaternion; -} - -namespace OEngine -{ - namespace Physic - { - class BulletShapeLoader; - } -} - -namespace CSVWorld -{ - // This class is just an extension of normal btRigidBody in order to add extra info. - // When bullet give back a btRigidBody, you can just do a static_cast to RigidBody, - // so one never should use btRigidBody directly! - class RigidBody: public btRigidBody - { - std::string mReferenceId; - - public: - - RigidBody(btRigidBody::btRigidBodyConstructionInfo &CI, const std::string &referenceId); - virtual ~RigidBody(); - - std::string getReferenceId() const { return mReferenceId; } - }; - - struct HeightField - { - btHeightfieldTerrainShape* mShape; - RigidBody* mBody; - }; - - // The PhysicsEngine class contain everything which is needed for Physic. - // It's needed that Ogre Resources are set up before the PhysicsEngine is created. - // Note:deleting it WILL NOT delete the RigidBody! - class PhysicsEngine - { - //Bullet Stuff - btBroadphaseInterface *mBroadphase; - btDefaultCollisionConfiguration *mCollisionConfiguration; - btSequentialImpulseConstraintSolver *mSolver; - btCollisionDispatcher *mDispatcher; - btDiscreteDynamicsWorld *mDynamicsWorld; - - //the NIF file loader. - OEngine::Physic::BulletShapeLoader *mShapeLoader; - - typedef std::map HeightFieldContainer; - HeightFieldContainer mHeightFieldMap; - - typedef std::map RigidBodyContainer; - RigidBodyContainer mCollisionObjectMap; - - RigidBodyContainer mRaycastingObjectMap; - - std::map mDebugDrawers; - std::map mDebugSceneNodes; - - enum CollisionType { - CollisionType_Nothing = 0, // rayTest(const btVector3 &from, - const btVector3 &to, bool raycastingObjectOnly = true, - bool ignoreHeightMap = false, Ogre::Vector3* normal = NULL); - - private: - - PhysicsEngine(const PhysicsEngine&); - PhysicsEngine& operator=(const PhysicsEngine&); - - // Create a debug rendering. It is called by setDebgRenderingMode if it's - // not created yet. - // Important Note: this will crash if the Render is not yet initialised! - void createDebugRendering(); - - // Set the debug rendering mode. 0 to turn it off. - // Important Note: this will crash if the Render is not yet initialised! - void setDebugRenderingMode(int mode); - }; -} -#endif // CSV_WORLD_PHYSICSENGINE_H From d5768af952b81ff4d1c78eece4e3a7037838c7fc Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 6 Nov 2014 03:51:18 +0100 Subject: [PATCH 077/303] Fix INT_MIN trade exploit (Fixes #2096) --- apps/openmw/mwgui/tradewindow.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/openmw/mwgui/tradewindow.cpp b/apps/openmw/mwgui/tradewindow.cpp index 9ef926bd3b..0f98598fd8 100644 --- a/apps/openmw/mwgui/tradewindow.cpp +++ b/apps/openmw/mwgui/tradewindow.cpp @@ -89,6 +89,7 @@ namespace MWGui mDecreaseButton->eventMouseButtonReleased += MyGUI::newDelegate(this, &TradeWindow::onBalanceButtonReleased); mTotalBalance->eventValueChanged += MyGUI::newDelegate(this, &TradeWindow::onBalanceValueChanged); + mTotalBalance->setMinValue(INT_MIN+1); // disallow INT_MIN since abs(INT_MIN) is undefined setCoord(400, 0, 400, 300); } @@ -448,6 +449,9 @@ namespace MWGui void TradeWindow::onIncreaseButtonTriggered() { + // prevent overflows, and prevent entering INT_MIN since abs(INT_MIN) is undefined + if (mCurrentBalance == INT_MAX || mCurrentBalance == INT_MIN+1) + return; if(mCurrentBalance<=-1) mCurrentBalance -= 1; if(mCurrentBalance>=1) mCurrentBalance += 1; updateLabels(); From bebb59ef1165d65f25be8e25d8c596b4b66acf45 Mon Sep 17 00:00:00 2001 From: Lukasz Gromanowski Date: Thu, 6 Nov 2014 10:51:32 +0100 Subject: [PATCH 078/303] Removed sending of travis notifications to my email. Signed-off-by: Lukasz Gromanowski --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c35d292019..233117718b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,6 @@ after_script: - if [ "${TRAVIS_OS_NAME}" = "linux" ]; then ./openmw_test_suite; fi notifications: recipients: - - lgromanowski+travis.ci@gmail.com - corrmage+travis-ci@gmail.com email: on_success: change From 4dd645559d69c1f19b80d4ccece830f8686736aa Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 6 Nov 2014 13:26:19 +0100 Subject: [PATCH 079/303] added ModeButton specialisation of PushButton for use in SceneToolMode --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/view/widget/modebutton.cpp | 10 ++++++++ apps/opencs/view/widget/modebutton.hpp | 28 +++++++++++++++++++++++ apps/opencs/view/widget/scenetoolmode.cpp | 28 +++++++++++++++-------- apps/opencs/view/widget/scenetoolmode.hpp | 8 ++++--- 5 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 apps/opencs/view/widget/modebutton.cpp create mode 100644 apps/opencs/view/widget/modebutton.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index e2e5aa2e5a..9d8a22f7c6 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -72,7 +72,7 @@ opencs_units_noqt (view/world ) opencs_units (view/widget - scenetoolbar scenetool scenetoolmode pushbutton scenetooltoggle scenetoolrun + scenetoolbar scenetool scenetoolmode pushbutton scenetooltoggle scenetoolrun modebutton ) opencs_units (view/render diff --git a/apps/opencs/view/widget/modebutton.cpp b/apps/opencs/view/widget/modebutton.cpp new file mode 100644 index 0000000000..56896b4220 --- /dev/null +++ b/apps/opencs/view/widget/modebutton.cpp @@ -0,0 +1,10 @@ + +#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) {} diff --git a/apps/opencs/view/widget/modebutton.hpp b/apps/opencs/view/widget/modebutton.hpp new file mode 100644 index 0000000000..ac14afc952 --- /dev/null +++ b/apps/opencs/view/widget/modebutton.hpp @@ -0,0 +1,28 @@ +#ifndef CSV_WIDGET_MODEBUTTON_H +#define CSV_WIDGET_MODEBUTTON_H + +#include "pushbutton.hpp" + +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 = 0); + + /// Default-Implementation: do nothing + virtual void activate (SceneToolbar *toolbar); + + /// Default-Implementation: do nothing + virtual void deactivate (SceneToolbar *toolbar); + }; +} + +#endif diff --git a/apps/opencs/view/widget/scenetoolmode.cpp b/apps/opencs/view/widget/scenetoolmode.cpp index caedfa3ee8..fd3c34a2b9 100644 --- a/apps/opencs/view/widget/scenetoolmode.cpp +++ b/apps/opencs/view/widget/scenetoolmode.cpp @@ -6,9 +6,9 @@ #include #include "scenetoolbar.hpp" -#include "pushbutton.hpp" +#include "modebutton.hpp" -void CSVWidget::SceneToolMode::adjustToolTip (const PushButton *activeMode) +void CSVWidget::SceneToolMode::adjustToolTip (const ModeButton *activeMode) { QString toolTip = mToolTip; @@ -21,7 +21,7 @@ void CSVWidget::SceneToolMode::adjustToolTip (const PushButton *activeMode) CSVWidget::SceneToolMode::SceneToolMode (SceneToolbar *parent, const QString& toolTip) : SceneTool (parent), mButtonSize (parent->getButtonSize()), mIconSize (parent->getIconSize()), - mToolTip (toolTip), mFirst (0) + mToolTip (toolTip), mFirst (0), mCurrent (0), mToolbar (parent) { mPanel = new QFrame (this, Qt::Popup); @@ -44,8 +44,7 @@ void CSVWidget::SceneToolMode::showPanel (const QPoint& position) void CSVWidget::SceneToolMode::addButton (const std::string& icon, const std::string& id, const QString& tooltip) { - PushButton *button = new PushButton (QIcon (QPixmap (icon.c_str())), PushButton::Type_Mode, - tooltip, mPanel); + ModeButton *button = new ModeButton (QIcon (QPixmap (icon.c_str())), tooltip, mPanel); button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); button->setIconSize (QSize (mIconSize, mIconSize)); button->setFixedSize (mButtonSize, mButtonSize); @@ -58,29 +57,40 @@ void CSVWidget::SceneToolMode::addButton (const std::string& icon, const std::st if (mButtons.size()==1) { - mFirst = button; + mFirst = mCurrent = button; setIcon (button->icon()); button->setChecked (true); adjustToolTip (button); + mCurrent->activate (mToolbar); } } void CSVWidget::SceneToolMode::selected() { - std::map::const_iterator iter = - mButtons.find (dynamic_cast (sender())); + std::map::const_iterator iter = + mButtons.find (dynamic_cast (sender())); if (iter!=mButtons.end()) { if (!iter->first->hasKeepOpen()) mPanel->hide(); - for (std::map::const_iterator iter2 = mButtons.begin(); + 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); } } \ No newline at end of file diff --git a/apps/opencs/view/widget/scenetoolmode.hpp b/apps/opencs/view/widget/scenetoolmode.hpp index 9959f98355..7fb7586f2d 100644 --- a/apps/opencs/view/widget/scenetoolmode.hpp +++ b/apps/opencs/view/widget/scenetoolmode.hpp @@ -10,7 +10,7 @@ class QHBoxLayout; namespace CSVWidget { class SceneToolbar; - class PushButton; + class ModeButton; ///< \brief Mode selector tool class SceneToolMode : public SceneTool @@ -19,13 +19,15 @@ namespace CSVWidget QWidget *mPanel; QHBoxLayout *mLayout; - std::map mButtons; // widget, id + std::map mButtons; // widget, id int mButtonSize; int mIconSize; QString mToolTip; PushButton *mFirst; + ModeButton *mCurrent; + SceneToolbar *mToolbar; - void adjustToolTip (const PushButton *activeMode); + void adjustToolTip (const ModeButton *activeMode); public: From 0fd33e319881f18a1fd039d52c377a3cbfcbb7a3 Mon Sep 17 00:00:00 2001 From: hasufell Date: Thu, 6 Nov 2014 22:27:35 +0100 Subject: [PATCH 080/303] Fix morrowind data files detection for custom install locations --- files/openmw.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/openmw.cfg b/files/openmw.cfg index e105b6304c..b60c1ba728 100644 --- a/files/openmw.cfg +++ b/files/openmw.cfg @@ -1,5 +1,5 @@ -data="?global?data" data="?mw?Data Files" +data=${MORROWIND_DATA_FILES} data-local="?userdata?data" resources=${OPENMW_RESOURCE_FILES} script-blacklist=Museum From 07ea352e46634892d1d8d4a863aba7d043b5b0d9 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 7 Nov 2014 00:16:41 +0100 Subject: [PATCH 081/303] Fix button graphics preventing mouse clicks (Fixes #2104) --- files/mygui/openmw_button.skin.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/files/mygui/openmw_button.skin.xml b/files/mygui/openmw_button.skin.xml index 73c266818b..18055a7e59 100644 --- a/files/mygui/openmw_button.skin.xml +++ b/files/mygui/openmw_button.skin.xml @@ -3,6 +3,7 @@ + @@ -12,6 +13,7 @@ + @@ -21,6 +23,7 @@ + @@ -30,6 +33,7 @@ + @@ -39,21 +43,25 @@ + + + + From 9e67a07ad47af80fbb8b0f7bad27e572d3c2d5fd Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Fri, 7 Nov 2014 11:11:43 +0100 Subject: [PATCH 082/303] allow externally supplied ModeButtons in SceneToolMode --- apps/opencs/view/widget/scenetoolmode.cpp | 7 +++++++ apps/opencs/view/widget/scenetoolmode.hpp | 3 +++ 2 files changed, 10 insertions(+) diff --git a/apps/opencs/view/widget/scenetoolmode.cpp b/apps/opencs/view/widget/scenetoolmode.cpp index fd3c34a2b9..8d871cc5f4 100644 --- a/apps/opencs/view/widget/scenetoolmode.cpp +++ b/apps/opencs/view/widget/scenetoolmode.cpp @@ -45,6 +45,13 @@ void CSVWidget::SceneToolMode::addButton (const std::string& icon, const std::st 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); diff --git a/apps/opencs/view/widget/scenetoolmode.hpp b/apps/opencs/view/widget/scenetoolmode.hpp index 7fb7586f2d..e6f462cf89 100644 --- a/apps/opencs/view/widget/scenetoolmode.hpp +++ b/apps/opencs/view/widget/scenetoolmode.hpp @@ -38,6 +38,9 @@ namespace CSVWidget 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); + signals: void modeChanged (const std::string& id); From b8d5a9486ac04270034126df753fe7e69d198b41 Mon Sep 17 00:00:00 2001 From: MiroslavR Date: Sun, 2 Nov 2014 18:01:12 +0100 Subject: [PATCH 083/303] Make Restore/Damage Attribute/Skill effects continuous --- apps/openmw/mwmechanics/actors.cpp | 12 +++++++++--- apps/openmw/mwmechanics/actors.hpp | 2 +- apps/openmw/mwmechanics/stat.hpp | 8 ++++---- components/esm/statstate.hpp | 14 ++++++++++---- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 02594258db..2e835d57e4 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -369,7 +369,7 @@ namespace MWMechanics void Actors::updateNpc (const MWWorld::Ptr& ptr, float duration) { updateDrowning(ptr, duration); - calculateNpcStatModifiers(ptr); + calculateNpcStatModifiers(ptr, duration); updateEquippedLight(ptr, duration); } @@ -499,6 +499,9 @@ namespace MWMechanics effects.get(EffectKey(ESM::MagicEffect::DrainAttribute, i)).getMagnitude() - effects.get(EffectKey(ESM::MagicEffect::AbsorbAttribute, i)).getMagnitude()); + stat.damage(effects.get(EffectKey(ESM::MagicEffect::DamageAttribute, i)).getMagnitude() * duration * 1.5); + stat.restore(effects.get(EffectKey(ESM::MagicEffect::RestoreAttribute, i)).getMagnitude() * duration * 1.5); + creatureStats.setAttribute(i, stat); } @@ -855,7 +858,7 @@ namespace MWMechanics } } - void Actors::calculateNpcStatModifiers (const MWWorld::Ptr& ptr) + void Actors::calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration) { NpcStats &npcStats = ptr.getClass().getNpcStats(ptr); const MagicEffects &effects = npcStats.getMagicEffects(); @@ -867,6 +870,9 @@ namespace MWMechanics skill.setModifier(effects.get(EffectKey(ESM::MagicEffect::FortifySkill, i)).getMagnitude() - effects.get(EffectKey(ESM::MagicEffect::DrainSkill, i)).getMagnitude() - effects.get(EffectKey(ESM::MagicEffect::AbsorbSkill, i)).getMagnitude()); + + skill.damage(effects.get(EffectKey(ESM::MagicEffect::DamageSkill, i)).getMagnitude() * duration * 1.5); + skill.restore(effects.get(EffectKey(ESM::MagicEffect::RestoreSkill, i)).getMagnitude() * duration * 1.5); } } @@ -1534,6 +1540,6 @@ namespace MWMechanics adjustMagicEffects(ptr); calculateCreatureStatModifiers(ptr, 0.f); if (ptr.getClass().isNpc()) - calculateNpcStatModifiers(ptr); + calculateNpcStatModifiers(ptr, 0.f); } } diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 55f1719f62..0ccfaad78a 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -34,7 +34,7 @@ namespace MWMechanics void calculateDynamicStats (const MWWorld::Ptr& ptr); void calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration); - void calculateNpcStatModifiers (const MWWorld::Ptr& ptr); + void calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration); void calculateRestoration (const MWWorld::Ptr& ptr, float duration); diff --git a/apps/openmw/mwmechanics/stat.hpp b/apps/openmw/mwmechanics/stat.hpp index 0fb4c57328..1c33db0fd2 100644 --- a/apps/openmw/mwmechanics/stat.hpp +++ b/apps/openmw/mwmechanics/stat.hpp @@ -236,20 +236,20 @@ namespace MWMechanics { int mBase; int mModifier; - int mDamage; + float mDamage; // needs to be float to allow continuous damage public: AttributeValue() : mBase(0), mModifier(0), mDamage(0) {} - int getModified() const { return std::max(0, mBase - mDamage + mModifier); } + int getModified() const { return std::max(0, mBase - (int) mDamage + mModifier); } int getBase() const { return mBase; } int getModifier() const { return mModifier; } void setBase(int base) { mBase = std::max(0, base); } void setModifier(int mod) { mModifier = mod; } - void damage(int damage) { mDamage += damage; } - void restore(int amount) { mDamage -= std::min(mDamage, amount); } + void damage(float damage) { mDamage += damage; } + void restore(float amount) { mDamage -= std::min(mDamage, amount); } int getDamage() const { return mDamage; } void writeState (ESM::StatState& state) const; diff --git a/components/esm/statstate.hpp b/components/esm/statstate.hpp index f1a3b4d794..801d0ce826 100644 --- a/components/esm/statstate.hpp +++ b/components/esm/statstate.hpp @@ -15,7 +15,7 @@ namespace ESM T mMod; // Note: can either be the modifier, or the modified value. // A bit inconsistent, but we can't fix this without breaking compatibility. T mCurrent; - T mDamage; + float mDamage; float mProgress; StatState(); @@ -36,8 +36,14 @@ namespace ESM esm.getHNOT (mMod, "STMO"); mCurrent = 0; esm.getHNOT (mCurrent, "STCU"); - mDamage = 0; - esm.getHNOT (mDamage, "STDA"); + + // mDamage was changed to a float; ensure backwards compatibility + T oldDamage = 0; + esm.getHNOT(oldDamage, "STDA"); + mDamage = oldDamage; + + esm.getHNOT (mDamage, "STDF"); + mProgress = 0; esm.getHNOT (mProgress, "STPR"); } @@ -54,7 +60,7 @@ namespace ESM esm.writeHNT ("STCU", mCurrent); if (mDamage) - esm.writeHNT ("STDA", mDamage); + esm.writeHNT ("STDF", mDamage); if (mProgress) esm.writeHNT ("STPR", mProgress); From 92ab292e18ec0379c9218dc0fc1458a7823c1efd Mon Sep 17 00:00:00 2001 From: scrawl Date: Sun, 9 Nov 2014 04:50:54 +0100 Subject: [PATCH 084/303] Make MODL subrecord for light records optional (Fixes #2114) --- components/esm/loadligh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/esm/loadligh.cpp b/components/esm/loadligh.cpp index c02bb46b69..f88ff09d6f 100644 --- a/components/esm/loadligh.cpp +++ b/components/esm/loadligh.cpp @@ -10,7 +10,7 @@ namespace ESM void Light::load(ESMReader &esm) { - mModel = esm.getHNString("MODL"); + mModel = esm.getHNOString("MODL"); mName = esm.getHNOString("FNAM"); mIcon = esm.getHNOString("ITEX"); assert(sizeof(mData) == 24); From b50fcd403bd8b74a2ebcb0d50c2271cb881cf7cd Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 9 Nov 2014 11:29:45 +0100 Subject: [PATCH 085/303] renamed getElementMask to getVisibilityMask --- apps/opencs/view/render/pagedworldspacewidget.cpp | 4 ++-- apps/opencs/view/render/pagedworldspacewidget.hpp | 2 +- apps/opencs/view/render/worldspacewidget.cpp | 4 ++-- apps/opencs/view/render/worldspacewidget.hpp | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index 8b143e93ff..9b42feb38f 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -430,9 +430,9 @@ CSVRender::WorldspaceWidget::dropRequirments CSVRender::PagedWorldspaceWidget::g } -unsigned int CSVRender::PagedWorldspaceWidget::getElementMask() const +unsigned int CSVRender::PagedWorldspaceWidget::getVisibilityMask() const { - return WorldspaceWidget::getElementMask() | mControlElements->getSelection(); + return WorldspaceWidget::getVisibilityMask() | mControlElements->getSelection(); } CSVWidget::SceneToolToggle *CSVRender::PagedWorldspaceWidget::makeControlVisibilitySelector ( diff --git a/apps/opencs/view/render/pagedworldspacewidget.hpp b/apps/opencs/view/render/pagedworldspacewidget.hpp index 17b6d10c53..ec2d5dc240 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.hpp +++ b/apps/opencs/view/render/pagedworldspacewidget.hpp @@ -75,7 +75,7 @@ namespace CSVRender virtual CSVWidget::SceneToolToggle *makeControlVisibilitySelector ( CSVWidget::SceneToolbar *parent); - virtual unsigned int getElementMask() const; + virtual unsigned int getVisibilityMask() const; protected: diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index ebb15ea2ce..0a8d5e364e 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -216,7 +216,7 @@ bool CSVRender::WorldspaceWidget::handleDrop (const std::vectorgetSelection(); } @@ -311,7 +311,7 @@ void CSVRender::WorldspaceWidget::debugProfileAboutToBeRemoved (const QModelInde void CSVRender::WorldspaceWidget::elementSelectionChanged() { - setVisibilityMask (getElementMask()); + setVisibilityMask (getVisibilityMask()); flagAsModified(); updateOverlay(); } diff --git a/apps/opencs/view/render/worldspacewidget.hpp b/apps/opencs/view/render/worldspacewidget.hpp index 4830d772b9..fb2f61f9ca 100644 --- a/apps/opencs/view/render/worldspacewidget.hpp +++ b/apps/opencs/view/render/worldspacewidget.hpp @@ -80,7 +80,7 @@ namespace CSVRender virtual bool handleDrop (const std::vector& data, DropType type); - virtual unsigned int getElementMask() const; + virtual unsigned int getVisibilityMask() const; protected: From fe385214e4d45969ababbebe76d419f4af6e602a Mon Sep 17 00:00:00 2001 From: Evgenii Babinets Date: Mon, 10 Nov 2014 02:42:44 -0500 Subject: [PATCH 086/303] Added proper slow fall effect mechanics. --- apps/openmw/mwworld/physicssystem.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 80cccd063c..eecc4a02db 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -472,10 +472,9 @@ namespace MWWorld physicActor->setInertialForce(Ogre::Vector3(0.0f)); else { - float diff = time*-627.2f; + inertia.z += time * -627.2f; if (inertia.z < 0) - diff *= slowFall; - inertia.z += diff; + inertia.z *= slowFall; physicActor->setInertialForce(inertia); } physicActor->setOnGround(isOnGround); @@ -865,8 +864,8 @@ namespace MWWorld continue; physicActor->setCanWaterWalk(waterCollision); - // 100 points of slowfall reduce gravity by 90% (this is just a guess) - float slowFall = 1-std::min(std::max(0.f, (effects.get(ESM::MagicEffect::SlowFall).getMagnitude() / 100.f) * 0.9f), 0.9f); + // Slow fall reduces fall speed by a factor of (effect magnitude / 200) + float slowFall = 1.f - std::max(0.f, std::min(1.f, effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f)); Ogre::Vector3 newpos = MovementSolver::move(iter->first, iter->second, mTimeAccum, world->isFlying(iter->first), From 3828e3fae506d71488dd0065061815291067ff21 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Mon, 10 Nov 2014 16:37:31 +0100 Subject: [PATCH 087/303] updated credits file --- credits.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/credits.txt b/credits.txt index 10065b44d1..d7fe230c7c 100644 --- a/credits.txt +++ b/credits.txt @@ -46,6 +46,7 @@ Jannik Heller (scrawl) Jason Hooks (jhooks) jeaye Jeffrey Haines (Jyby) +Jengerer Joel Graff (graffy) John Blomberg (fstp) Jordan Ayers From 2acf446f18a042eb245febc87333a8eab16588b1 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 11 Nov 2014 15:58:22 +0100 Subject: [PATCH 088/303] added edit mode button to scene toolbar --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/view/render/editmode.cpp | 19 +++++++++++ apps/opencs/view/render/editmode.hpp | 28 ++++++++++++++++ apps/opencs/view/render/worldspacewidget.cpp | 34 +++++++++++++++++++- apps/opencs/view/render/worldspacewidget.hpp | 15 +++++++++ apps/opencs/view/world/scenesubview.cpp | 3 ++ 6 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 apps/opencs/view/render/editmode.cpp create mode 100644 apps/opencs/view/render/editmode.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 9d8a22f7c6..5990c09420 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -77,7 +77,7 @@ opencs_units (view/widget opencs_units (view/render scenewidget worldspacewidget pagedworldspacewidget unpagedworldspacewidget - previewwidget + previewwidget editmode ) opencs_units_noqt (view/render diff --git a/apps/opencs/view/render/editmode.cpp b/apps/opencs/view/render/editmode.cpp new file mode 100644 index 0000000000..51a137d3b3 --- /dev/null +++ b/apps/opencs/view/render/editmode.cpp @@ -0,0 +1,19 @@ + +#include "editmode.hpp" + +#include "worldspacewidget.hpp" + +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); +} \ No newline at end of file diff --git a/apps/opencs/view/render/editmode.hpp b/apps/opencs/view/render/editmode.hpp new file mode 100644 index 0000000000..c3192f8ea5 --- /dev/null +++ b/apps/opencs/view/render/editmode.hpp @@ -0,0 +1,28 @@ +#ifndef CSV_RENDER_EDITMODE_H +#define CSV_RENDER_EDITMODE_H + +#include "../widget/modebutton.hpp" + +namespace CSVRender +{ + class WorldspaceWidget; + + class EditMode : public CSVWidget::ModeButton + { + Q_OBJECT + + WorldspaceWidget *mWorldspaceWidget; + unsigned int mMask; + + public: + + EditMode (WorldspaceWidget *worldspaceWidget, const QIcon& icon, unsigned int mask, + const QString& tooltip = "", QWidget *parent = 0); + + unsigned int getInteractionMask() const; + + virtual void activate (CSVWidget::SceneToolbar *toolbar); + }; +} + +#endif diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 0a8d5e364e..e70b456352 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -16,10 +16,11 @@ #include "../widget/scenetooltoggle.hpp" #include "../widget/scenetoolrun.hpp" +#include "editmode.hpp" #include "elements.hpp" CSVRender::WorldspaceWidget::WorldspaceWidget (CSMDoc::Document& document, QWidget* parent) -: SceneWidget (parent), mDocument(document), mSceneElements(0), mRun(0) +: SceneWidget (parent), mDocument(document), mSceneElements(0), mRun(0), mInteractionMask (0) { setAcceptDrops(true); @@ -162,6 +163,16 @@ CSVWidget::SceneToolRun *CSVRender::WorldspaceWidget::makeRunTool ( return mRun; } +CSVWidget::SceneToolMode *CSVRender::WorldspaceWidget::makeEditModeSelector ( + CSVWidget::SceneToolbar *parent) +{ + CSVWidget::SceneToolMode *tool = new CSVWidget::SceneToolMode (parent, "Edit Mode"); + + addEditModeSelectorButtons (tool); + + return tool; +} + CSVRender::WorldspaceWidget::DropType CSVRender::WorldspaceWidget::getDropType ( const std::vector< CSMWorld::UniversalId >& data) { @@ -221,6 +232,16 @@ unsigned int CSVRender::WorldspaceWidget::getVisibilityMask() const return mSceneElements->getSelection(); } +void CSVRender::WorldspaceWidget::setInteractionMask (unsigned int mask) +{ + mInteractionMask = mask | Element_CellMarker | Element_CellArrow; +} + +unsigned int CSVRender::WorldspaceWidget::getInteractionMask() const +{ + return mInteractionMask & getVisibilityMask(); +} + void CSVRender::WorldspaceWidget::addVisibilitySelectorButtons ( CSVWidget::SceneToolToggle *tool) { @@ -230,6 +251,17 @@ void CSVRender::WorldspaceWidget::addVisibilitySelectorButtons ( tool->addButton (":armor.png", Element_Pathgrid, ":armor.png", "Pathgrid"); } +void CSVRender::WorldspaceWidget::addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool) +{ + /// \todo replace EditMode with suitable subclasses + tool->addButton ( + new EditMode (this, QIcon (":armor.png"), Element_Reference, "Reference editing"), + "object"); + tool->addButton ( + new EditMode (this, QIcon (":armor.png"), Element_Pathgrid, "Pathgrid editing"), + "pathgrid"); +} + CSMDoc::Document& CSVRender::WorldspaceWidget::getDocument() { return mDocument; diff --git a/apps/opencs/view/render/worldspacewidget.hpp b/apps/opencs/view/render/worldspacewidget.hpp index fb2f61f9ca..9477b29912 100644 --- a/apps/opencs/view/render/worldspacewidget.hpp +++ b/apps/opencs/view/render/worldspacewidget.hpp @@ -33,6 +33,7 @@ namespace CSVRender CSVWidget::SceneToolToggle *mSceneElements; CSVWidget::SceneToolRun *mRun; CSMDoc::Document& mDocument; + unsigned int mInteractionMask; public: @@ -67,6 +68,10 @@ namespace CSVRender /// 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(); static DropType getDropType(const std::vector& data); @@ -82,10 +87,20 @@ namespace CSVRender 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; + protected: virtual void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle *tool); + virtual void addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool); + CSMDoc::Document& getDocument(); virtual void updateOverlay(); diff --git a/apps/opencs/view/world/scenesubview.cpp b/apps/opencs/view/world/scenesubview.cpp index ce68da3623..f2c987b0f7 100644 --- a/apps/opencs/view/world/scenesubview.cpp +++ b/apps/opencs/view/world/scenesubview.cpp @@ -125,6 +125,9 @@ CSVWidget::SceneToolbar* CSVWorld::SceneSubView::makeToolbar (CSVRender::Worldsp CSVWidget::SceneToolRun *runTool = widget->makeRunTool (toolbar); toolbar->addTool (runTool); + CSVWidget::SceneToolMode *editModeTool = widget->makeEditModeSelector (toolbar); + toolbar->addTool (editModeTool); + return toolbar; } From b86148411ba04167d847130553de4f84092a2302 Mon Sep 17 00:00:00 2001 From: cc9cii Date: Wed, 12 Nov 2014 14:02:08 +1100 Subject: [PATCH 089/303] Fix for issue #2051. Not perfect (can get into a lock in some situations) but usable. --- apps/opencs/view/render/navigation1st.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/opencs/view/render/navigation1st.cpp b/apps/opencs/view/render/navigation1st.cpp index 91f88634af..5d9a034688 100644 --- a/apps/opencs/view/render/navigation1st.cpp +++ b/apps/opencs/view/render/navigation1st.cpp @@ -44,10 +44,11 @@ bool CSVRender::Navigation1st::mouseMoved (const QPoint& delta, int mode) float deltaPitch = getFactor (true) * delta.y(); Ogre::Radian newPitch = oldPitch + Ogre::Degree (deltaPitch); - Ogre::Radian limit (Ogre::Math::PI/2-0.5); - - if ((deltaPitch>0 && newPitch-limit)) + if ((deltaPitch>0 && newPitchOgre::Radian(0.5))) + { mCamera->pitch (Ogre::Degree (deltaPitch)); + } } return true; From b7f8f848a8af3fa22843a501e26e6d55de720bfb Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 13 Nov 2014 10:45:59 +0100 Subject: [PATCH 090/303] added edit modes for exteriors --- .../view/render/pagedworldspacewidget.cpp | 22 +++++++++++++++++++ .../view/render/pagedworldspacewidget.hpp | 2 ++ 2 files changed, 24 insertions(+) diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index 9b42feb38f..a2360f9478 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -21,7 +21,9 @@ #include "../../model/world/idtable.hpp" #include "../widget/scenetooltoggle.hpp" +#include "../widget/scenetoolmode.hpp" +#include "editmode.hpp" #include "elements.hpp" bool CSVRender::PagedWorldspaceWidget::adjustCells() @@ -194,6 +196,26 @@ void CSVRender::PagedWorldspaceWidget::mouseDoubleClickEvent (QMouseEvent *event } } +void CSVRender::PagedWorldspaceWidget::addEditModeSelectorButtons ( + CSVWidget::SceneToolMode *tool) +{ + WorldspaceWidget::addEditModeSelectorButtons (tool); + + /// \todo replace EditMode with suitable subclasses + tool->addButton ( + new EditMode (this, QIcon (":armor.png"), Element_Reference, "Terrain shape editing"), + "terrain-shape"); + tool->addButton ( + new EditMode (this, QIcon (":armor.png"), Element_Reference, "Terrain texture editing"), + "terrain-texture"); + tool->addButton ( + new EditMode (this, QIcon (":armor.png"), Element_Reference, "Terrain vertex paint editing"), + "terrain-vertex"); + tool->addButton ( + new EditMode (this, QIcon (":armor.png"), Element_Reference, "Terrain movement"), + "terrain-move"); +} + void CSVRender::PagedWorldspaceWidget::updateOverlay() { if(getCamera()->getViewport()) diff --git a/apps/opencs/view/render/pagedworldspacewidget.hpp b/apps/opencs/view/render/pagedworldspacewidget.hpp index ec2d5dc240..bc627ed422 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.hpp +++ b/apps/opencs/view/render/pagedworldspacewidget.hpp @@ -79,6 +79,8 @@ namespace CSVRender protected: + virtual void addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool); + virtual void updateOverlay(); virtual void mouseReleaseEvent (QMouseEvent *event); From 5eb9fd81e1dc620bdf9c14cedd230ffa8813545b Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 13 Nov 2014 12:09:10 +0100 Subject: [PATCH 091/303] use placeholder icons in OpenCS when no proper icon is available --- apps/opencs/model/world/universalid.cpp | 2 +- .../view/render/pagedworldspacewidget.cpp | 16 ++++++++-------- apps/opencs/view/render/worldspacewidget.cpp | 16 ++++++++-------- credits.txt | 2 +- files/opencs/placeholder.png | Bin 0 -> 2383 bytes files/opencs/resources.qrc | 1 + 6 files changed, 19 insertions(+), 18 deletions(-) create mode 100644 files/opencs/placeholder.png diff --git a/apps/opencs/model/world/universalid.cpp b/apps/opencs/model/world/universalid.cpp index f2ed87d61a..190ea87d8a 100644 --- a/apps/opencs/model/world/universalid.cpp +++ b/apps/opencs/model/world/universalid.cpp @@ -328,7 +328,7 @@ std::string CSMWorld::UniversalId::getIcon() const for (int i=0; typeData[i].mName; ++i) if (typeData[i].mType==mType) - return typeData[i].mIcon ? typeData[i].mIcon : ""; + return typeData[i].mIcon ? typeData[i].mIcon : ":placeholder"; throw std::logic_error ("failed to retrieve UniversalId type icon"); } diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index 66ea26f11a..e5d5428581 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -219,16 +219,16 @@ void CSVRender::PagedWorldspaceWidget::addEditModeSelectorButtons ( /// \todo replace EditMode with suitable subclasses tool->addButton ( - new EditMode (this, QIcon (":armor.png"), Element_Reference, "Terrain shape editing"), + new EditMode (this, QIcon (":placeholder"), Element_Reference, "Terrain shape editing"), "terrain-shape"); tool->addButton ( - new EditMode (this, QIcon (":armor.png"), Element_Reference, "Terrain texture editing"), + new EditMode (this, QIcon (":placeholder"), Element_Reference, "Terrain texture editing"), "terrain-texture"); tool->addButton ( - new EditMode (this, QIcon (":armor.png"), Element_Reference, "Terrain vertex paint editing"), + new EditMode (this, QIcon (":placeholder"), Element_Reference, "Terrain vertex paint editing"), "terrain-vertex"); tool->addButton ( - new EditMode (this, QIcon (":armor.png"), Element_Reference, "Terrain movement"), + new EditMode (this, QIcon (":placeholder"), Element_Reference, "Terrain movement"), "terrain-move"); } @@ -476,12 +476,12 @@ CSVWidget::SceneToolToggle *CSVRender::PagedWorldspaceWidget::makeControlVisibil CSVWidget::SceneToolbar *parent) { mControlElements = new CSVWidget::SceneToolToggle (parent, - "Controls & Guides Visibility", ":door.png"); + "Controls & Guides Visibility", ":placeholder"); - mControlElements->addButton (":activator.png", Element_CellMarker, ":activator.png", + mControlElements->addButton (":placeholder", Element_CellMarker, ":placeholder", "Cell marker"); - mControlElements->addButton (":armor.png", Element_CellArrow, ":armor.png", "Cell arrows"); - mControlElements->addButton (":armor.png", Element_CellBorder, ":armor.png", "Cell border"); + mControlElements->addButton (":placeholder", Element_CellArrow, ":placeholder", "Cell arrows"); + mControlElements->addButton (":placeholder", Element_CellBorder, ":placeholder", "Cell border"); mControlElements->setSelection (0xffffffff); diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 534325e848..6c6acd22dc 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -130,7 +130,7 @@ CSVWidget::SceneToolMode *CSVRender::WorldspaceWidget::makeNavigationSelector ( CSVWidget::SceneToolToggle *CSVRender::WorldspaceWidget::makeSceneVisibilitySelector (CSVWidget::SceneToolbar *parent) { mSceneElements= new CSVWidget::SceneToolToggle (parent, - "Scene Element Visibility", ":door.png"); + "Scene Element Visibility", ":placeholder"); addVisibilitySelectorButtons (mSceneElements); @@ -172,7 +172,7 @@ CSVWidget::SceneToolRun *CSVRender::WorldspaceWidget::makeRunTool ( std::sort (profiles.begin(), profiles.end()); mRun = new CSVWidget::SceneToolRun (parent, "Run OpenMW from the current camera position", - ":door.png", ":faction.png", profiles); + ":placeholder", ":placeholder", profiles); connect (mRun, SIGNAL (runRequest (const std::string&)), this, SLOT (runRequest (const std::string&))); @@ -262,20 +262,20 @@ unsigned int CSVRender::WorldspaceWidget::getInteractionMask() const void CSVRender::WorldspaceWidget::addVisibilitySelectorButtons ( CSVWidget::SceneToolToggle *tool) { - tool->addButton (":activator.png", Element_Reference, ":activator.png", "References"); - tool->addButton (":armor.png", Element_Terrain, ":armor.png", "Terrain"); - tool->addButton (":armor.png", Element_Water, ":armor.png", "Water"); - tool->addButton (":armor.png", Element_Pathgrid, ":armor.png", "Pathgrid"); + tool->addButton (":placeholder", Element_Reference, ":placeholder", "References"); + tool->addButton (":placeholder", Element_Terrain, ":placeholder", "Terrain"); + tool->addButton (":placeholder", Element_Water, ":placeholder", "Water"); + tool->addButton (":placeholder", Element_Pathgrid, ":placeholder", "Pathgrid"); } void CSVRender::WorldspaceWidget::addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool) { /// \todo replace EditMode with suitable subclasses tool->addButton ( - new EditMode (this, QIcon (":armor.png"), Element_Reference, "Reference editing"), + new EditMode (this, QIcon (":placeholder"), Element_Reference, "Reference editing"), "object"); tool->addButton ( - new EditMode (this, QIcon (":armor.png"), Element_Pathgrid, "Pathgrid editing"), + new EditMode (this, QIcon (":placeholder"), Element_Pathgrid, "Pathgrid editing"), "pathgrid"); } diff --git a/credits.txt b/credits.txt index d7fe230c7c..c6a5c71696 100644 --- a/credits.txt +++ b/credits.txt @@ -140,7 +140,7 @@ Sadler Artwork: Necrod - OpenMW Logo Mickey Lyle (raevol) - Wordpress Theme -Tom Koenderink (Okulo), SirHerrbatka, crysthala - OpenMW Editor Icons +Tom Koenderink (Okulo), SirHerrbatka, crysthala, Shnatsel - OpenMW Editor Icons Inactive Contributors: Ardekantur diff --git a/files/opencs/placeholder.png b/files/opencs/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..1d3df3c47d76bd213e958a44e752e4693e537f32 GIT binary patch literal 2383 zcmV-V39$BwP)DsmHZ}0aX5fB&{$e=-k zc>etPZ|_^~=!;V6ckT7{^^Ff8m!s3^*uH%`b#-+%{bVv3EnBukDwQIYN_qG09cFXW z@x@^QTCJAxTrrc9YaRaF)F`T3&l{{H@q9Xl4SR*O_BrM|u%v)N2dO%2D7 zALrDmQ@nfk?)M9rI&~`V-@k9vr+xeOOrAWM`1p9MwqCt@k(88#i;D|-y`GsfXY%63 z3!&Z5&yR@{Cvy1kVUm-RxpwWE=ok+D14D)k!OzdHNdYrw&gA6DlNbyJ3;T!>BiOQK z3wQ6{B_<|@%*;#+@&Ki!rEJ)+0i8}qQc@BF2M+vw0;WxyMsaa5A3l7r>C>f47lscX z&Vd65EbOtdu_Pxa)3$9}#*7(5L_|cRV-*z@v1!vL7A;yN{CCX~5EvMUr>7@3Z{BQl z24>BgMOIc8A3uH+Y~kVIBqStITU*PbMT>}visI6xOHGcMpPx@{Z7o4TLCqJCkdVOc z-Mdk#RJ?liO4P^S-=BW{`jMTTE!aFgJ<;iOm`o;?EnCK|TeqmKt>x3FPj-!4TwIK| zw|DadgocKqR4Tc6@giPcUQ|_8iTcf%Gl$H~Od)JarBYU}UX4G zm6g$@OBcrlNTpH|5)#PB$e_Ny9xpF1s;jH5{DOjl@bdEF#EBDvO{>)s7#PUWqenS^ z{`@bK2PiKuZ=QhBqeoL$SI6bcmyyfmxVpMpMGk=3vuBf$k-^umUj^Ir>C>sHsUagH z!y)qUeZ!Q=WL9>&1Sk{=;^X5AO1(gd4}i;GoWyGjUzR!5io;@il zDza*8R6ys>ozZHwWM*bs*u`cbDk_STCr?`Sq0wlXEg(8Nn%vx6(QLL+0rTh2XWzbk zR_k4%P*`k0YPA|4A0Mtz2dh<#IXE(a{_|dQ`OCrT~pbgIcZT z$dMyfZ8DjRZ{NNN&ykUloH=s_lgVV^=k4u{!CN;5Bqk;@fByVN_YW%p?b@{?Ha51=lU7|_ z9W7h71YqdUp%fGpi2A*K`xcc-^{d>`Xf&)`xssHW6wwd!BdnIt?Afz9ckZ0%rqO_^ zsw&iKHSk@4t(kiH@+Ce#J}8w+yNJu>a^}vRi%zFw<;s;d`?V>cO`A3Z1qHEZ&z?qK zs;;g^tya^fO&ffDeYt)6wy0luc{zD`d8}HsN|-S*C=?1NO`62v!-w(m@**xSj{EoT zH%T5q@^fkN=LjSEl0?FpGiOLoPe-HCuypBCo4#_noE0lpFm&ip^7HelsHou0n>T#= z^a&pyAN>6M2nYz^#*G^sI&_Ffj~>}Y+@=6~v2o)@`u6S1u3fuq#^@iRQmGg=Y#1IM z9=N%=(W+G|Dl02_`0yb`MMb=R{n`;4jtlVf^CKxKiG>RnHY>|)E*uu{|AN28-9HUc z0cmMzbm-9GpXTBaLV-McrTp`jR!M)Z2UP=K8@X%bhiTw&a}aTp8+^78U5DtHQoLa2E*RB-ClDZyvJfB{G(5-YBvGY}USN9)$Dk;~7Gf986hR z8P3km00ajIvwr=0K7Ra2r%s(nPfr)pg~Y@}7B61RlP6E;-@iX=)~vDUyRNQ|QKLpN zXwV?RwqU^m;Ctn6!GZ;pmzNU~62h`&%K%78Nx{v{jojQ^)~#FD^b9m0BqRidLc!w2 zi!JQ2v9aviw@=u>cI?xqtzMk0|2$Hl8xuQFl61mS=3^z;P4WHMngneg!Nuus63FJCa5 z%~VuWkeZsxwr$%ucI+6NH*W@D@7}%0WHPd|vq?`+=fsHTL^Bsg#_Y z9OlfKV|9ZglgW_Fedit-preview.png edit-clone.png add.png + placeholder.png raster/startup/big/create-addon.png From 95dfb078160832560addd7ed065226459df87514 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 15 Nov 2014 09:53:08 +0100 Subject: [PATCH 092/303] make [ a whitespace character in scripts (Fixes #2126) --- components/compiler/scanner.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/compiler/scanner.cpp b/components/compiler/scanner.cpp index e73477ee7e..16d54ff51d 100644 --- a/components/compiler/scanner.cpp +++ b/components/compiler/scanner.cpp @@ -512,7 +512,8 @@ namespace Compiler bool Scanner::isWhitespace (char c) { - return c==' ' || c=='\t'; + return c==' ' || c=='\t' + || c=='['; ///< \todo disable this when doing more strict compiling } // constructor From 21481e8c7179b693ad204b07d35f4e9266fafffb Mon Sep 17 00:00:00 2001 From: scrawl Date: Sun, 16 Nov 2014 18:08:36 +0100 Subject: [PATCH 093/303] Fix bsatool help typo --- apps/bsatool/bsatool.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/bsatool/bsatool.cpp b/apps/bsatool/bsatool.cpp index 3781dd066e..7e47d0b8fa 100644 --- a/apps/bsatool/bsatool.cpp +++ b/apps/bsatool/bsatool.cpp @@ -51,7 +51,7 @@ bool parseOptions (int argc, char** argv, Arguments &info) ("help,h", "print help message.") ("version,v", "print version information and quit.") ("long,l", "Include extra information in archive listing.") - ("full-path,f", "Create diretory hierarchy on file extraction " + ("full-path,f", "Create directory hierarchy on file extraction " "(always true for extractall).") ; From 3028141815cedba10462f4e54bbe2da79f26e50b Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 18 Nov 2014 11:47:46 +0100 Subject: [PATCH 094/303] Set up ESMReader indices in OpenCS (fixes wrong terrain textures when multiple content files are loaded) --- apps/opencs/model/world/data.cpp | 3 ++- apps/opencs/model/world/data.hpp | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index c11594207c..9cb0299c4c 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -60,7 +60,7 @@ int CSMWorld::Data::count (RecordBase::State state, const CollectionBase& collec CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourcesManager) : mEncoder (encoding), mPathgrids (mCells), mRefs (mCells), - mResourcesManager (resourcesManager), mReader (0), mDialogue (0) + mResourcesManager (resourcesManager), mReader (0), mDialogue (0), mReaderIndex(0) { mGlobals.addColumn (new StringIdColumn); mGlobals.addColumn (new RecordStateColumn); @@ -659,6 +659,7 @@ int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base mReader = new ESM::ESMReader; mReader->setEncoder (&mEncoder); + mReader->setIndex(mReaderIndex++); mReader->open (path.string()); mBase = base; diff --git a/apps/opencs/model/world/data.hpp b/apps/opencs/model/world/data.hpp index 70a02656b4..37d4d4b8a7 100644 --- a/apps/opencs/model/world/data.hpp +++ b/apps/opencs/model/world/data.hpp @@ -98,6 +98,7 @@ namespace CSMWorld bool mBase; bool mProject; std::map > mRefLoadCache; + int mReaderIndex; std::vector > mReaders; From 751211351ce75c009d48fb57b3c432571e1ab929 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 18 Nov 2014 17:54:24 +0100 Subject: [PATCH 095/303] Fix multi-line comment warning --- components/process/processinvoker.cpp | 56 ++++++++++++++------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/components/process/processinvoker.cpp b/components/process/processinvoker.cpp index 44a11cc4e2..4d53084c6d 100644 --- a/components/process/processinvoker.cpp +++ b/components/process/processinvoker.cpp @@ -111,37 +111,39 @@ bool Process::ProcessInvoker::startProcess(const QString &name, const QStringLis } else { mProcess->start(path, arguments); -// if (!mProcess->waitForFinished()) { -// QMessageBox msgBox; -// msgBox.setWindowTitle(tr("Error starting executable")); -// msgBox.setIcon(QMessageBox::Critical); -// msgBox.setStandardButtons(QMessageBox::Ok); -// msgBox.setText(tr("

Could not start %1

\ -//

An error occurred while starting %1.

\ -//

Press \"Show Details...\" for more information.

").arg(info.fileName())); -// msgBox.setDetailedText(mProcess->errorString()); -// msgBox.exec(); + /* + if (!mProcess->waitForFinished()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error starting executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not start %1

\ +

An error occurred while starting %1.

\ +

Press \"Show Details...\" for more information.

").arg(info.fileName())); + msgBox.setDetailedText(mProcess->errorString()); + msgBox.exec(); -// return false; -// } + return false; + } -// if (mProcess->exitCode() != 0 || mProcess->exitStatus() == QProcess::CrashExit) { -// QString error(mProcess->readAllStandardError()); -// error.append(tr("\nArguments:\n")); -// error.append(arguments.join(" ")); + if (mProcess->exitCode() != 0 || mProcess->exitStatus() == QProcess::CrashExit) { + QString error(mProcess->readAllStandardError()); + error.append(tr("\nArguments:\n")); + error.append(arguments.join(" ")); -// QMessageBox msgBox; -// msgBox.setWindowTitle(tr("Error running executable")); -// msgBox.setIcon(QMessageBox::Critical); -// msgBox.setStandardButtons(QMessageBox::Ok); -// msgBox.setText(tr("

Executable %1 returned an error

\ -//

An error occurred while running %1.

\ -//

Press \"Show Details...\" for more information.

").arg(info.fileName())); -// msgBox.setDetailedText(error); -// msgBox.exec(); + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error running executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Executable %1 returned an error

\ +

An error occurred while running %1.

\ +

Press \"Show Details...\" for more information.

").arg(info.fileName())); + msgBox.setDetailedText(error); + msgBox.exec(); -// return false; -// } + return false; + } + */ } return true; From d4a9cf70757605ca8a7e378cc987665be4f66a6e Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 18 Nov 2014 17:54:37 +0100 Subject: [PATCH 096/303] Fix wizard install location --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 84fc5043ae..208bddf33a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -435,7 +435,7 @@ IF(NOT WIN32 AND NOT APPLE) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/niftest" DESTINATION "${BINDIR}" ) ENDIF(BUILD_NIFTEST) IF(BUILD_WIZARD) - INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/wizard" DESTINATION "${BINDIR}" ) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/openmw-wizard" DESTINATION "${BINDIR}" ) ENDIF(BUILD_WIZARD) if(BUILD_MYGUI_PLUGIN) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Plugin_MyGUI_OpenMW_Resources.so" DESTINATION "${LIBDIR}" ) From e5a6049ea7d2955edc4512f9900c0a49a6c476bd Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 18 Nov 2014 18:03:31 +0100 Subject: [PATCH 097/303] Improve clarity of label --- files/ui/wizard/methodselectionpage.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/ui/wizard/methodselectionpage.ui b/files/ui/wizard/methodselectionpage.ui index 122ef754e5..531d093afb 100644 --- a/files/ui/wizard/methodselectionpage.ui +++ b/files/ui/wizard/methodselectionpage.ui @@ -79,7 +79,7 @@ - Install Morrowind to a new location for OpenMW to use. + Install Morrowind from a retail disk to a new location for OpenMW to use. true From ede1b97b4658156b8287f150e46f46a38fc44dae Mon Sep 17 00:00:00 2001 From: cc9cii Date: Wed, 19 Nov 2014 18:10:29 +1000 Subject: [PATCH 098/303] Check the type of universal id before getting its id. Should resolve bug #2137 --- apps/opencs/view/doc/view.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index 8b5efbea7f..8dd8494dca 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -474,8 +474,13 @@ void CSVDoc::View::addSubView (const CSMWorld::UniversalId& id, const std::strin { foreach(SubView *sb, mSubViews) { - if((isReferenceable && (CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Referenceable, id.getId()) == CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Referenceable, sb->getUniversalId().getId()))) - || (!isReferenceable && (id == sb->getUniversalId()))) + bool isSubViewReferenceable = + sb->getUniversalId().getType() == CSMWorld::UniversalId::Type_Referenceable; + + if((isReferenceable && isSubViewReferenceable && + id.getId() == sb->getUniversalId().getId()) + || + (!isReferenceable && id == sb->getUniversalId())) { sb->setFocus(Qt::OtherFocusReason); // FIXME: focus not quite working return; From c6558fe487b6c53d6be6a470de9061bb11fc0082 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 19 Nov 2014 13:19:02 +0100 Subject: [PATCH 099/303] Fix Ogre being initialised twice --- apps/launcher/graphicspage.cpp | 3 ++- components/ogreinit/ogreinit.cpp | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index 1c6e69023d..ec7f5a04d7 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -137,6 +137,7 @@ bool Launcher::GraphicsPage::setupSDL() return false; } + screenComboBox->clear(); for (int i = 0; i < displays; i++) { screenComboBox->addItem(QString(tr("Screen ")) + QString::number(i + 1)); @@ -149,7 +150,7 @@ bool Launcher::GraphicsPage::loadSettings() { if (!setupSDL()) return false; - if (!setupOgre()) + if (!mOgre && !setupOgre()) return false; if (mGraphicsSettings.value(QString("Video/vsync")) == QLatin1String("true")) diff --git a/components/ogreinit/ogreinit.cpp b/components/ogreinit/ogreinit.cpp index 40712e2821..574b67f19d 100644 --- a/components/ogreinit/ogreinit.cpp +++ b/components/ogreinit/ogreinit.cpp @@ -94,6 +94,8 @@ namespace OgreInit Ogre::Root* OgreInit::init(const std::string &logPath) { + if (mRoot) + throw std::runtime_error("OgreInit was already initialised"); #ifndef ANDROID // Set up logging first From 86037149cd9e9d7c871721dc1c46a4c58346a468 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 20 Nov 2014 11:06:32 +0100 Subject: [PATCH 100/303] add optional string argument for activate instruction (ignored) --- components/compiler/extensions0.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index 8a17b5e79d..7531cdd5bf 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -240,7 +240,7 @@ namespace Compiler { extensions.registerFunction ("xbox", 'l', "", opcodeXBox); extensions.registerFunction ("onactivate", 'l', "", opcodeOnActivate); - extensions.registerInstruction ("activate", "", opcodeActivate, opcodeActivateExplicit); + extensions.registerInstruction ("activate", "x", opcodeActivate, opcodeActivateExplicit); extensions.registerInstruction ("lock", "/l", opcodeLock, opcodeLockExplicit); extensions.registerInstruction ("unlock", "", opcodeUnlock, opcodeUnlockExplicit); extensions.registerInstruction ("cast", "SS", opcodeCast, opcodeCastExplicit); From 31fab533fe1f6db2634c2abff5f5697e76f7e2fd Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 20 Nov 2014 18:14:49 +0100 Subject: [PATCH 101/303] OSX build fix --- components/process/processinvoker.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/components/process/processinvoker.cpp b/components/process/processinvoker.cpp index 4d53084c6d..cc842fd615 100644 --- a/components/process/processinvoker.cpp +++ b/components/process/processinvoker.cpp @@ -7,6 +7,7 @@ #include #include #include +#include Process::ProcessInvoker::ProcessInvoker() { From 911839bb91b3e2dfa7e675493b47784abd9877b5 Mon Sep 17 00:00:00 2001 From: cc9cii Date: Fri, 21 Nov 2014 06:48:42 +1100 Subject: [PATCH 102/303] Simplify detecting whether a universal id represents a referenceable type. --- apps/opencs/view/doc/view.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index 8dd8494dca..d64d36aec7 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -464,8 +464,7 @@ void CSVDoc::View::addSubView (const CSMWorld::UniversalId& id, const std::strin { CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance(); - const std::vector referenceables(CSMWorld::UniversalId::listReferenceableTypes()); - bool isReferenceable = std::find(referenceables.begin(), referenceables.end(), id.getType()) != referenceables.end(); + bool isReferenceable = id.getClass() == CSMWorld::UniversalId::Class_RefRecord; // User setting to reuse sub views (on a per top level view basis) bool reuse = @@ -477,12 +476,11 @@ void CSVDoc::View::addSubView (const CSMWorld::UniversalId& id, const std::strin bool isSubViewReferenceable = sb->getUniversalId().getType() == CSMWorld::UniversalId::Type_Referenceable; - if((isReferenceable && isSubViewReferenceable && - id.getId() == sb->getUniversalId().getId()) + if((isReferenceable && isSubViewReferenceable && id.getId() == sb->getUniversalId().getId()) || (!isReferenceable && id == sb->getUniversalId())) { - sb->setFocus(Qt::OtherFocusReason); // FIXME: focus not quite working + sb->setFocus(); return; } } @@ -502,7 +500,7 @@ void CSVDoc::View::addSubView (const CSMWorld::UniversalId& id, const std::strin } SubView *view = NULL; - if(std::find(referenceables.begin(), referenceables.end(), id.getType()) != referenceables.end()) + if(isReferenceable) { view = mSubViewFactory.makeSubView (CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Referenceable, id.getId()), *mDocument); } From e11bec1a37956a4f5ef07f84e07629b8da932855 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 20 Nov 2014 22:36:24 +0100 Subject: [PATCH 103/303] Fix the launcher not properly reloading data directories --- components/config/gamesettings.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/components/config/gamesettings.cpp b/components/config/gamesettings.cpp index 5c348b1910..1e7f716e21 100644 --- a/components/config/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -38,9 +38,6 @@ Config::GameSettings::~GameSettings() void Config::GameSettings::validatePaths() { - if (mSettings.isEmpty() || !mDataDirs.isEmpty()) - return; // Don't re-validate paths if they are already parsed - QStringList paths = mSettings.values(QString("data")); Files::PathContainer dataDirs; From 1e06f69d9e0a33f2e92f4451fabae3393ec1d267 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 20 Nov 2014 22:39:21 +0100 Subject: [PATCH 104/303] Remove the "browse to existing installation" button, now replaced by the wizard Clean up some debug spam --- apps/launcher/maindialog.cpp | 28 ++-------------------------- apps/launcher/settingspage.cpp | 4 ---- apps/wizard/inisettings.cpp | 3 --- apps/wizard/installationpage.cpp | 2 -- apps/wizard/mainwizard.cpp | 2 -- 5 files changed, 2 insertions(+), 37 deletions(-) diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 41a23e2464..975958d7ac 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -388,27 +388,14 @@ bool Launcher::MainDialog::setupGameSettings() msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Cancel); msgBox.setText(tr("
Could not find the Data Files location

\ - The directory containing the data files was not found.

\ - Press \"Browse...\" to specify the location manually.
")); - - QAbstractButton *browseButton = - msgBox.addButton(tr("Browse..."), QMessageBox::ActionRole); + The directory containing the data files was not found.")); QAbstractButton *wizardButton = msgBox.addButton(tr("Run &Installation Wizard..."), QMessageBox::ActionRole); msgBox.exec(); - QString selectedFile; - if (msgBox.clickedButton() == browseButton) - { - selectedFile = QFileDialog::getOpenFileName( - this, - tr("Select master file"), - QDir::currentPath(), - tr("Morrowind master file (*.esm)")); - } - else if (msgBox.clickedButton() == wizardButton) + if (msgBox.clickedButton() == wizardButton) { if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) { return false; @@ -416,15 +403,6 @@ bool Launcher::MainDialog::setupGameSettings() return true; } } - - if (selectedFile.isEmpty()) - return false; // Cancel was clicked - - QFileInfo info(selectedFile); - - // Add the new dir to the settings file and to the data dir container - mGameSettings.setMultiValue(QString("data"), info.absolutePath()); - mGameSettings.addDataDir(info.absolutePath()); } return true; @@ -610,14 +588,12 @@ bool Launcher::MainDialog::writeSettings() void Launcher::MainDialog::closeEvent(QCloseEvent *event) { - qDebug() << "close event!"; writeSettings(); event->accept(); } void Launcher::MainDialog::wizardStarted() { - qDebug() << "wizard started!"; hide(); } diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 45e0f72a44..5422e79574 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -174,13 +174,11 @@ void Launcher::SettingsPage::wizardStarted() { mMain->hide(); // Hide the launcher - qDebug() << "wizard started!"; wizardButton->setEnabled(false); } void Launcher::SettingsPage::wizardFinished(int exitCode, QProcess::ExitStatus exitStatus) { - qDebug() << "wizard finished!"; if (exitCode != 0 || exitStatus == QProcess::CrashExit) return qApp->quit(); @@ -192,13 +190,11 @@ void Launcher::SettingsPage::wizardFinished(int exitCode, QProcess::ExitStatus e void Launcher::SettingsPage::importerStarted() { - qDebug() << "importer started!"; importerButton->setEnabled(false); } void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus exitStatus) { - qDebug() << "importer finished!"; if (exitCode != 0 || exitStatus == QProcess::CrashExit) return; diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp index f67da04557..3711ba0669 100644 --- a/apps/wizard/inisettings.cpp +++ b/apps/wizard/inisettings.cpp @@ -33,7 +33,6 @@ QStringList Wizard::IniSettings::findKeys(const QString &text) bool Wizard::IniSettings::readFile(QTextStream &stream) { - qDebug() << "readFile called!"; // Look for a square bracket, "'\\[" // that has one or more "not nothing" in it, "([^]]+)" // and is closed with a square bracket, "\\]" @@ -66,7 +65,6 @@ bool Wizard::IniSettings::readFile(QTextStream &stream) if (!currentSection.isEmpty()) key = currentSection + QLatin1Char('/') + key; - //qDebug() << "adding: " << key << value; mSettings[key] = QVariant(value); } } @@ -204,7 +202,6 @@ bool Wizard::IniSettings::parseInx(const QString &path) const QString key(array.mid(section.length() + 3, lenght)); QString value(array.mid(section.length() + key.length() + 6)); - //qDebug() << section << key << value; // Add the value setValue(section + QLatin1Char('/') + key, QVariant(value)); diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index b76aeea228..09e5773173 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -58,8 +58,6 @@ Wizard::InstallationPage::InstallationPage(QWidget *parent) : Wizard::InstallationPage::~InstallationPage() { - qDebug() << "stop!"; - if (mThread->isRunning()) { mUnshield->stopWorker(); mThread->wait(); diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index e587d77e54..d539793eca 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -331,12 +331,10 @@ void Wizard::MainWizard::setupPages() void Wizard::MainWizard::importerStarted() { - qDebug() << "importer started!"; } void Wizard::MainWizard::importerFinished(int exitCode, QProcess::ExitStatus exitStatus) { - qDebug() << "importer finished!"; if (exitCode != 0 || exitStatus == QProcess::CrashExit) return; From 31fdf4961676712bb9af4bc7d905248931aba6a2 Mon Sep 17 00:00:00 2001 From: cc9cii Date: Sat, 22 Nov 2014 06:59:23 +1100 Subject: [PATCH 105/303] Use Qt exit function rather than system one. --- apps/opencs/editor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 591667ebb7..bef83b8ac7 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -379,5 +379,5 @@ void CS::Editor::documentAdded (CSMDoc::Document *document) void CS::Editor::lastDocumentDeleted() { - exit (0); + QApplication::quit(); } From 414f7ea02cea63ac239fa480d189090dcbcb9fb2 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 19 Nov 2014 12:09:40 +0100 Subject: [PATCH 106/303] Ignore mouse movements during video playback (Fixes #2139) --- apps/openmw/mwinput/inputmanagerimp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 29b166a6a7..6cb3a5ec59 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -603,7 +603,7 @@ namespace MWInput MyGUI::InputManager::getInstance().injectMouseMove( int(mMouseX), int(mMouseY), mMouseWheel); } - if (mMouseLookEnabled) + if (mMouseLookEnabled && !mControlsDisabled) { resetIdleTime(); From a0c454b01c711a57cde3363e98f9fb5c44ac58dc Mon Sep 17 00:00:00 2001 From: Nikolay Kasyanov Date: Sat, 22 Nov 2014 22:53:01 +0200 Subject: [PATCH 107/303] Properly add libunshiled include dir (again, for wizard this time) --- apps/wizard/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index f2defd031b..8e9ea8d0e0 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -82,7 +82,7 @@ QT4_WRAP_UI(UI_HDRS ${WIZARD_UI}) include(${QT_USE_FILE}) -include_directories(${CMAKE_CURRENT_BINARY_DIR} ${LIBUNSHIELD_INCLUDE}) +include_directories(${CMAKE_CURRENT_BINARY_DIR} ${LIBUNSHIELD_INCLUDE_DIR}) add_executable(openmw-wizard ${GUI_TYPE} From a8a2b44b0fe52df0dbef027c3ac2b8eba38e01fa Mon Sep 17 00:00:00 2001 From: scrawl Date: Sun, 23 Nov 2014 00:51:57 +0100 Subject: [PATCH 108/303] Don't require libunshield on windows --- CMakeLists.txt | 5 ---- apps/wizard/CMakeLists.txt | 42 ++++++++++++++++++++--------- apps/wizard/mainwizard.cpp | 2 ++ apps/wizard/methodselectionpage.cpp | 5 ++++ 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 208bddf33a..5a923c6312 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -600,11 +600,6 @@ if (BUILD_OPENCS) endif() if (BUILD_WIZARD) - find_package(LIBUNSHIELD REQUIRED) - if(NOT LIBUNSHIELD_FOUND) - message(FATAL_ERROR "Failed to find Unshield library") - endif(NOT LIBUNSHIELD_FOUND) - add_subdirectory(apps/wizard) endif() diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index f2defd031b..76ca648dbb 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -1,10 +1,20 @@ +if (WIN32) # windows users can just run the morrowind installer + set(OPENMW_USE_UNSHIELD FALSE) +else() + set(OPENMW_USE_UNSHIELD TRUE) + + find_package(LIBUNSHIELD REQUIRED) + if(NOT LIBUNSHIELD_FOUND) + message(FATAL_ERROR "Failed to find Unshield library") + endif(NOT LIBUNSHIELD_FOUND) +endif() + set(WIZARD componentselectionpage.cpp conclusionpage.cpp existinginstallationpage.cpp importpage.cpp inisettings.cpp - installationpage.cpp installationtargetpage.cpp intropage.cpp languageselectionpage.cpp @@ -12,8 +22,6 @@ set(WIZARD mainwizard.cpp methodselectionpage.cpp - unshield/unshieldworker.cpp - utils/componentlistwidget.cpp ) @@ -23,15 +31,12 @@ set(WIZARD_HEADER existinginstallationpage.hpp importpage.hpp inisettings.hpp - installationpage.hpp installationtargetpage.hpp intropage.hpp languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp - unshield/unshieldworker.hpp - utils/componentlistwidget.hpp ) @@ -41,15 +46,12 @@ set(WIZARD_HEADER_MOC conclusionpage.hpp existinginstallationpage.hpp importpage.hpp - installationpage.hpp installationtargetpage.hpp intropage.hpp languageselectionpage.hpp mainwizard.hpp methodselectionpage.hpp - unshield/unshieldworker.hpp - utils/componentlistwidget.hpp ) @@ -58,13 +60,21 @@ set(WIZARD_UI ${CMAKE_SOURCE_DIR}/files/ui/wizard/conclusionpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/existinginstallationpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/importpage.ui - ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationtargetpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/intropage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/languageselectionpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/methodselectionpage.ui ) +if (OPENMW_USE_UNSHIELD) + set (WIZARD ${WIZARD} installationpage.cpp unshield/unshieldworker.cpp) + set (WIZARD_HEADER ${WIZARD_HEADER} installationpage.hpp unshield/unshieldworker.hpp) + set (WIZARD_HEADER_MOC ${WIZARD_HEADER_MOC} installationpage.hpp unshield/unshieldworker.hpp) + set (WIZARD_UI ${WIZARD_UI} ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationpage.ui) + add_definitions(-DOPENMW_USE_UNSHIELD) +endif (OPENMW_USE_UNSHIELD) + + source_group(wizard FILES ${WIZARD} ${WIZARD_HEADER}) find_package(Qt4 REQUIRED) @@ -82,7 +92,11 @@ QT4_WRAP_UI(UI_HDRS ${WIZARD_UI}) include(${QT_USE_FILE}) -include_directories(${CMAKE_CURRENT_BINARY_DIR} ${LIBUNSHIELD_INCLUDE}) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +if (OPENMW_USE_UNSHIELD) + include_directories(${LIBUNSHIELD_INCLUDE}) +endif() add_executable(openmw-wizard ${GUI_TYPE} @@ -96,10 +110,14 @@ add_executable(openmw-wizard target_link_libraries(openmw-wizard ${Boost_LIBRARIES} ${QT_LIBRARIES} - ${LIBUNSHIELD_LIBRARY} components ) +if (OPENMW_USE_UNSHIELD) + target_link_libraries(openmw-wizard ${LIBUNSHIELD_LIBRARY}) +endif() + + if(DPKG_PROGRAM) INSTALL(TARGETS openmw-wizard RUNTIME DESTINATION games COMPONENT openmw-wizard) endif() diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index d539793eca..f98aae3152 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -322,7 +322,9 @@ void Wizard::MainWizard::setupPages() setPage(Page_ExistingInstallation, new ExistingInstallationPage(this)); setPage(Page_InstallationTarget, new InstallationTargetPage(this, mCfgMgr)); setPage(Page_ComponentSelection, new ComponentSelectionPage(this)); +#ifdef OPENMW_USE_UNSHIELD setPage(Page_Installation, new InstallationPage(this)); +#endif setPage(Page_Import, new ImportPage(this)); setPage(Page_Conclusion, new ConclusionPage(this)); setStartId(Page_Intro); diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp index d7c64f3b03..5f3917bd51 100644 --- a/apps/wizard/methodselectionpage.cpp +++ b/apps/wizard/methodselectionpage.cpp @@ -9,6 +9,11 @@ Wizard::MethodSelectionPage::MethodSelectionPage(QWidget *parent) : setupUi(this); +#ifndef OPENMW_USE_UNSHIELD + newLocationRadioButton->setEnabled(false); + existingLocationRadioButton->setChecked(true); +#endif + registerField(QLatin1String("installation.new"), newLocationRadioButton); } From abf49267ea1ed1e79b8774dec8023b18aa5a85ab Mon Sep 17 00:00:00 2001 From: scrawl Date: Sun, 23 Nov 2014 13:44:03 +0100 Subject: [PATCH 109/303] Unshield include fix --- apps/wizard/mainwizard.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index f98aae3152..e68070c4d5 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -15,10 +15,13 @@ #include "existinginstallationpage.hpp" #include "installationtargetpage.hpp" #include "componentselectionpage.hpp" -#include "installationpage.hpp" #include "importpage.hpp" #include "conclusionpage.hpp" +#ifdef OPENMW_USE_UNSHIELD +#include "installationpage.hpp" +#endif + using namespace Process; Wizard::MainWizard::MainWizard(QWidget *parent) : From c9e7a41e1a77e18e810e1e3a0e67b1a0a9c401c3 Mon Sep 17 00:00:00 2001 From: Sergey Shnatsel Davidoff Date: Sun, 23 Nov 2014 17:39:01 +0300 Subject: [PATCH 110/303] Add my scene view icons: status overview icons, element toggle icons, parts from which they're assembled and the SVG master. --- .../scene-view-parts/0_sky_backdrop.png | Bin 0 -> 216 bytes .../raster/scene-view-parts/10_bridge.png | Bin 0 -> 1848 bytes .../scene-view-parts/11_terrain_front.png | Bin 0 -> 1215 bytes .../scene-view-parts/12_water_front.png | Bin 0 -> 593 bytes .../raster/scene-view-parts/13_pathgrid.png | Bin 0 -> 751 bytes .../raster/scene-view-parts/14_divider.png | Bin 0 -> 224 bytes .../1_terrain_very_distant.png | Bin 0 -> 906 bytes .../scene-view-parts/2_fog_very_distant.png | Bin 0 -> 334 bytes .../scene-view-parts/3_water_backdrop.png | Bin 0 -> 824 bytes .../scene-view-parts/4_water_distant.png | Bin 0 -> 668 bytes .../scene-view-parts/5_terrain_distant.png | Bin 0 -> 1152 bytes .../raster/scene-view-parts/6_fog_distant.png | Bin 0 -> 1541 bytes .../scene-view-parts/7_terrain_back.png | Bin 0 -> 1567 bytes .../raster/scene-view-parts/8_water_back.png | Bin 0 -> 638 bytes .../raster/scene-view-parts/9_fog_back.png | Bin 0 -> 1426 bytes files/opencs/raster/scene-view-parts/README | 10 + .../composite_the_32_icons.sh | 21 + files/opencs/raster/scene-view-parts/mask.png | Bin 0 -> 3419 bytes .../scalable/scene-view-status-rev14.svg | 923 ++++++++++++++++++ files/opencs/scene-view-fog.png | Bin 0 -> 962 bytes files/opencs/scene-view-pathgrid.png | Bin 0 -> 640 bytes files/opencs/scene-view-references.png | Bin 0 -> 1785 bytes files/opencs/scene-view-status-0.png | Bin 0 -> 2029 bytes files/opencs/scene-view-status-1.png | Bin 0 -> 2787 bytes files/opencs/scene-view-status-10.png | Bin 0 -> 2589 bytes files/opencs/scene-view-status-11.png | Bin 0 -> 3186 bytes files/opencs/scene-view-status-12.png | Bin 0 -> 3025 bytes files/opencs/scene-view-status-13.png | Bin 0 -> 3279 bytes files/opencs/scene-view-status-14.png | Bin 0 -> 3133 bytes files/opencs/scene-view-status-15.png | Bin 0 -> 3354 bytes files/opencs/scene-view-status-16.png | Bin 0 -> 3078 bytes files/opencs/scene-view-status-17.png | Bin 0 -> 3265 bytes files/opencs/scene-view-status-18.png | Bin 0 -> 3201 bytes files/opencs/scene-view-status-19.png | Bin 0 -> 3327 bytes files/opencs/scene-view-status-2.png | Bin 0 -> 2296 bytes files/opencs/scene-view-status-20.png | Bin 0 -> 3256 bytes files/opencs/scene-view-status-21.png | Bin 0 -> 3398 bytes files/opencs/scene-view-status-22.png | Bin 0 -> 3389 bytes files/opencs/scene-view-status-23.png | Bin 0 -> 3464 bytes files/opencs/scene-view-status-24.png | Bin 0 -> 3249 bytes files/opencs/scene-view-status-25.png | Bin 0 -> 3355 bytes files/opencs/scene-view-status-26.png | Bin 0 -> 3351 bytes files/opencs/scene-view-status-27.png | Bin 0 -> 3426 bytes files/opencs/scene-view-status-28.png | Bin 0 -> 3388 bytes files/opencs/scene-view-status-29.png | Bin 0 -> 3491 bytes files/opencs/scene-view-status-3.png | Bin 0 -> 2927 bytes files/opencs/scene-view-status-30.png | Bin 0 -> 3489 bytes files/opencs/scene-view-status-31.png | Bin 0 -> 3553 bytes files/opencs/scene-view-status-4.png | Bin 0 -> 2708 bytes files/opencs/scene-view-status-5.png | Bin 0 -> 3049 bytes files/opencs/scene-view-status-6.png | Bin 0 -> 2877 bytes files/opencs/scene-view-status-7.png | Bin 0 -> 3144 bytes files/opencs/scene-view-status-8.png | Bin 0 -> 2432 bytes files/opencs/scene-view-status-9.png | Bin 0 -> 3096 bytes files/opencs/scene-view-terrain.png | Bin 0 -> 2490 bytes files/opencs/scene-view-water.png | Bin 0 -> 1066 bytes 56 files changed, 954 insertions(+) create mode 100644 files/opencs/raster/scene-view-parts/0_sky_backdrop.png create mode 100644 files/opencs/raster/scene-view-parts/10_bridge.png create mode 100644 files/opencs/raster/scene-view-parts/11_terrain_front.png create mode 100644 files/opencs/raster/scene-view-parts/12_water_front.png create mode 100644 files/opencs/raster/scene-view-parts/13_pathgrid.png create mode 100644 files/opencs/raster/scene-view-parts/14_divider.png create mode 100644 files/opencs/raster/scene-view-parts/1_terrain_very_distant.png create mode 100644 files/opencs/raster/scene-view-parts/2_fog_very_distant.png create mode 100644 files/opencs/raster/scene-view-parts/3_water_backdrop.png create mode 100644 files/opencs/raster/scene-view-parts/4_water_distant.png create mode 100644 files/opencs/raster/scene-view-parts/5_terrain_distant.png create mode 100644 files/opencs/raster/scene-view-parts/6_fog_distant.png create mode 100644 files/opencs/raster/scene-view-parts/7_terrain_back.png create mode 100644 files/opencs/raster/scene-view-parts/8_water_back.png create mode 100644 files/opencs/raster/scene-view-parts/9_fog_back.png create mode 100644 files/opencs/raster/scene-view-parts/README create mode 100755 files/opencs/raster/scene-view-parts/composite_the_32_icons.sh create mode 100644 files/opencs/raster/scene-view-parts/mask.png create mode 100644 files/opencs/scalable/scene-view-status-rev14.svg create mode 100644 files/opencs/scene-view-fog.png create mode 100644 files/opencs/scene-view-pathgrid.png create mode 100644 files/opencs/scene-view-references.png create mode 100644 files/opencs/scene-view-status-0.png create mode 100644 files/opencs/scene-view-status-1.png create mode 100644 files/opencs/scene-view-status-10.png create mode 100644 files/opencs/scene-view-status-11.png create mode 100644 files/opencs/scene-view-status-12.png create mode 100644 files/opencs/scene-view-status-13.png create mode 100644 files/opencs/scene-view-status-14.png create mode 100644 files/opencs/scene-view-status-15.png create mode 100644 files/opencs/scene-view-status-16.png create mode 100644 files/opencs/scene-view-status-17.png create mode 100644 files/opencs/scene-view-status-18.png create mode 100644 files/opencs/scene-view-status-19.png create mode 100644 files/opencs/scene-view-status-2.png create mode 100644 files/opencs/scene-view-status-20.png create mode 100644 files/opencs/scene-view-status-21.png create mode 100644 files/opencs/scene-view-status-22.png create mode 100644 files/opencs/scene-view-status-23.png create mode 100644 files/opencs/scene-view-status-24.png create mode 100644 files/opencs/scene-view-status-25.png create mode 100644 files/opencs/scene-view-status-26.png create mode 100644 files/opencs/scene-view-status-27.png create mode 100644 files/opencs/scene-view-status-28.png create mode 100644 files/opencs/scene-view-status-29.png create mode 100644 files/opencs/scene-view-status-3.png create mode 100644 files/opencs/scene-view-status-30.png create mode 100644 files/opencs/scene-view-status-31.png create mode 100644 files/opencs/scene-view-status-4.png create mode 100644 files/opencs/scene-view-status-5.png create mode 100644 files/opencs/scene-view-status-6.png create mode 100644 files/opencs/scene-view-status-7.png create mode 100644 files/opencs/scene-view-status-8.png create mode 100644 files/opencs/scene-view-status-9.png create mode 100644 files/opencs/scene-view-terrain.png create mode 100644 files/opencs/scene-view-water.png diff --git a/files/opencs/raster/scene-view-parts/0_sky_backdrop.png b/files/opencs/raster/scene-view-parts/0_sky_backdrop.png new file mode 100644 index 0000000000000000000000000000000000000000..f215662fc472efd87778863ff81ac9bddcf9ac64 GIT binary patch literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmSQK*5Dp-y;YjHK@;M7UB8!3Q zuY)k7lg8`{prB-lYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt-XRE{-7? zjc?C8@*YrNIN;!*V)XHNw(!Q|sh<9h*57xY|FLl?^S&&HY!YBK=& Nc)I$ztaD0e0su}cLV5rI literal 0 HcmV?d00001 diff --git a/files/opencs/raster/scene-view-parts/10_bridge.png b/files/opencs/raster/scene-view-parts/10_bridge.png new file mode 100644 index 0000000000000000000000000000000000000000..1c9fd34f99579855652050151c8c95236225175c GIT binary patch literal 1848 zcmV-82gmq{P)$WJ6O#%eyrrs6kszuvXmsu1rx+vNtuno0R9N0+%$&w{gq9yBVeJoM7 z6Fy2L-+XQteDCQ!lA;l}01T~z+Dnaz&(9QJEd5=G^_7rgLTz5Rt)IPX>U;B$E`+A#+2!aGa z1VIGyl@bvGe06{V3fSRUqQPKeoH~02%rMMgHZaW4S|c?(_g3tBxadA!T=!9oZWZHe2{X&mqALGRHc)yX^GgZ2_2QCcg0fqsI9 z&8`UJw@m4IseM<(b0_*C1Yvx78Uv?KL6psqXQ7`azq??VkGT!cpXkR?FY`YC3*iu? zpeV1+wsOo(K4MTIZ9n+Wd8OW8>n3&CI1AegCCI$G&}FW~{8Q zVRAZsUn%ph09mP-nLpXKefmh(VR8kymIJ_6TR{W_D5X5RghC+@6P8w1Ev3|ZgV*m| z0f39=2Y$79|Nh^4x;xv52y^obMF4-eI&}Mz6K3k-g|nf%_vSu$>Fk-f8+$yJ$||Yf z9{g^|C6tmdYy_I45BnrE_FtWJLP=ee~JHV`dI1 zC84AQ0P>IJTpw6#1+6q}X@?|*4qh9ch+G+(==|i9*#nR$yD(fSt&r52IZQWwY^dct{tm_IS^lIQ+ozd zZ@#%Fp}i1^gaeszV;Ez+QcmP)q2fUcRa~btnG8jC?`Hr2+txJ)k4A$Qa$`uP2*BD`M8(tgpR5QTvALvw(g$eo8GR&A-BHq=MDIW)9T@VCG5#>u8^HWckBwCXiLmR}LR37#}o mGTGNGWm9CYv8oFAHsQZUpEj1=M literal 0 HcmV?d00001 diff --git a/files/opencs/raster/scene-view-parts/11_terrain_front.png b/files/opencs/raster/scene-view-parts/11_terrain_front.png new file mode 100644 index 0000000000000000000000000000000000000000..adab37570666a78a452d2691633e8037e5898c7f GIT binary patch literal 1215 zcmV;w1VHz;dKa?u1E3)_SgHt|t}oQUM+ z-o%io7y=?%*hImAm4AU?p{1Qw8k>k$VwI3W2ob?r1i?a4keJ-ZdCbgS7PHTsb5Eie zqZtD88x9PMIWxa+ug4x3F*6=!Y1;HykS2hg@UfVR~Ew5<-HZFK-`s{?3T z9YEXa0NPdu(6%~&w$%Z&tq!1VJ@f#wdhE!%z~h+RM&t_c-TLJ#+ie@Wo7PtPDX<^2 zqX_#zev`9jj+u*;aZxcWN~s4v5GH2ropcQ5ri*lZdi1 zHNEW=2s~+}a#+duxQ5zhubi^~+aU`wDVmrT-zqg{6g59$Cyu;s_N%1Vp3; z@Mf5=0lXPzP*t8^Z*Ec))lY^UT)%wv)`K*@w$e`pu0@d^+PAz&>>@-6<^@qfMbNPP z7}URz4H4M8r$?T}Pl(9(q5d9}c@?;kChpL_C1xv5cu3VJ-BCA4oKq)LP`>)NvxO((C75Uw{ z=-K6^OklXERUEY2w48WNTRBBK;!Il9w)i_T~6~jGudXRknxY zkIKrwy^uP|64x}UYM3-4)8?IvyV;b~Wc9&(xhxCv~AOy|HJGker~d&<0j+M9pIdt3J2QwB0Cv`UGqTuOf0g%a4h!-$CU&mAsVb_PtIAu9s;BW!J}SD;uvrBGZyohB+|M@42)^Xm{=&I`hHChFk9<0dx04YMis+x zNtPv~i3_H6#<(K-&$oygLIDpbKA>btf{L&?7?ULtc^VTDG5~dNg!-Sn+qn6e-@srD z$0b{%0u`aRkm6hvjBBVx4Vk$2{q0VOIwwgSqoNE(WB%9}vXI5(sjFAtYU?rq5ODJ| z-^>^lC7Xi+Gsu#dIG$Pc=3PXa*uPcXlnPqgpJXF)5j{;Q%96pjWH2gN%oDQ20n$t< z%y#xbSykNL8dFxDIF3jr#>0cOF@FE%8weG|D=Gw#Ix8N*IY&>GqNvyyjM*L*?CPZ? daa8{o;ZG_Ii0wRLUq}D|002ovPDHLkV1ko~Iza#c literal 0 HcmV?d00001 diff --git a/files/opencs/raster/scene-view-parts/12_water_front.png b/files/opencs/raster/scene-view-parts/12_water_front.png new file mode 100644 index 0000000000000000000000000000000000000000..cb5c7a3a07545d8abe3255341d8acd3e14f5e206 GIT binary patch literal 593 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmSQK*5Dp-y;YjHK@;M7UB8!3Q zuY)k7lg8`{prB-lYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&3=E9tJY5_^ zG8*5`+@F8hLFD-U_j-NF-D*3WN(Gr89Y53{5Z!KMT;d|8T;KK7era;*M|Ok52imlr zdATYCZfEwKy5!)_JBCN5t~%lSYRe_zOTT-be}8xH`Q84bPv#u{-}6ZC!L|+tbEN~l z90kfucZgCk%Dp^zVp{OyoY=VK zzQ2w>{+3qa&+6qkH8`EG|I}rml=ieY%oZmu|Mg<{++)(ZHGp>?463FvrFd43$Az7%9y-wm$A20B0~-H2UY*aj>?TH+DZ)h z411UsgqJ>?H2cI_L2Kp$jt9l_j@64j|H+=ybmaI~$0sSk=x6YB^>bP0l+XkKh@l39 literal 0 HcmV?d00001 diff --git a/files/opencs/raster/scene-view-parts/13_pathgrid.png b/files/opencs/raster/scene-view-parts/13_pathgrid.png new file mode 100644 index 0000000000000000000000000000000000000000..531fd018f621cd52faa35708a9a045e0723ddd7e GIT binary patch literal 751 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmSQK*5Dp-y;YjHK@;M7UB8!3Q zuY)k7lg8`{prB-lYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&3=B*;o-U3d z8I5meczc8dN;K>*et#ol+T<=XRWTRmpc{8M_Xf`9IMq|O^qSI3$1Plkqqb+|tL*5O zb4ks*a7amhb(2T6XOIAMqH;*qg6lOb=jL8cO538AyYPvp)%{QQ-~XTgfA{zN>W5sK zs@{^$rOe!}+$-F0qqKWWs;cre+pgBDi@b{7I)P0r>iFm9sS|Dao;HXd@NsFEjh|(| z{qLU(2WI)Q{%l^T-xoUlY4M_yYo5e*Hg+7G!kEc+!X;-`;PK}lpKrU$$Gb*X>xBFU zJL|C6ZRck)6{KhR`W#v;^W?=E*>hjxUq~xmZ}=-$@vN2Ofc1l93k5y7%>6HSrKNs~ zx?D0l-(*@SJ5wU(2Q$I@lWs6i&@?>su))I2doFix>ebiGi90O?rJH-@He7nh=<(c~ z(OM#s&xB!XtZl3EN0o`{3+}Y22Sy|wIG1wh=49Qe`EfU^gSbW8cUiw$A8N9+WhrN% z`M)Jc&vToqN3E)gzg->tI_K`SyOlq)T#XwT13BNarMN9LG1fEW*>LyOc}A%g*`6&- z?4SP5zZai*t2{GxnlS4U#<_Y=Bm$-|u2nc5Wd8ehk8;@T05%)34QE7X`!?JI%S_dj|o)DX^4Ng!aZ@ie%Xw4 zzx(?{yJxa1+Bxl)X-axs>Hhbtb2)2I$^qvIle41cD^|*}R%G|xx4v^b*vhu*5TDzn zVBuX=T>JI&x<99%+kPSF;s3uv|Ftt1uVNyd%0~G!&-Ps3aj|&^FmW~}U&Kt-{hE{-7? zjc?C83N{!p956Um|LQ{LE=~z8H@n`3d!=vouAh9*^84m%7FYNCxBkD>aE%1`jnVB6 Wd+eKE9p``+F?hQAxvXQ>s2C1!D$f(hF0jHZM@`0 zF;^4|PMj60AD~~L6gukIfk+3PsUuqnp=g{HNxydF{12NP>je+?abUO9Wlw%5Q zJ$=6L>+I}ov6Jh+4ch8;dh+Dh2@&R8=&vBi4_mSqRqFMX|HYXz8=6!xk0%zLo0}|?e!(^OjU=6296hy<#II?CrEBjf2epX6RO7=RT{;mM1IB`5le3SBoP?mwQNfB7z{H&CF{>C{7;e@o!+nP|v~22p6;!F|oz zW;fXg=LG-u(mc(Jeo;yUAXIf1exJH`V_O1~6DK|+#w7sXB$1`zW>Swr3k3jG5YbM= zzm6;~@#IBsu~0cOH8u11pz&|*LZ?r*FR;P2z0D-9Cn2xqH2@F*>{9Rm5QP@Qdy-^+ zp?`~rjt(mFb?3n7=xD&KS4KW=)`lAKn&96T!;RP&!=Id<`0C3+W7kFZ`H@4P9{8w{ zMd6Mf5%0`UBW@7$yMD!2eDnqVe(ZnziLVKw7HYGSHTYHkqj>f>+Aa0{|9b5E&t2a$<;y z31mc-iHM1WRo@6e&9kM<{W&u;lWr=O>er%YDgobA0=}sPd{YVdrV{W?CE%M%z&DkE gZz=)beDDJQ0Ti%0*dIP%0RR9107*qoM6N<$f_?&>p#T5? literal 0 HcmV?d00001 diff --git a/files/opencs/raster/scene-view-parts/2_fog_very_distant.png b/files/opencs/raster/scene-view-parts/2_fog_very_distant.png new file mode 100644 index 0000000000000000000000000000000000000000..f75794e782de14eab91cd91896b047e7701a27e2 GIT binary patch literal 334 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmSQK*5Dp-y;YjHK@;M7UB8!3Q zuY)k7lg8`{prB-lYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt-oLT^vI) z8n4bcEqKI$$B9*G{^xJ^y}LX`XWvwbZsDKj&YHx)8GXEP$$=A<^KQRUber(*yj69F z;FFvCe}j1UQ@6FQnVP$>>!w&$NDg;w`3;sh)7cF%Z+ISv?%2A4^F{0Wi@T46Es&bW z7`l0N%kF76oqH5CH^!&rZsvTTn_?c_D6!6ghkv89x`IVx#2cQ757rZAtaVs@gUh6@ z_mZUE1^rFxjlY2i9c$*LZQwS(U97a=}=wsXFwe;(uNR>7kagM6D3`pCV0PYhVzw`qicpk4aZRwOwj{!G{ve80QZVq;c z_8)Ho&t1=YsHK7UTxc4&1eo55HGmqh7vT literal 0 HcmV?d00001 diff --git a/files/opencs/raster/scene-view-parts/4_water_distant.png b/files/opencs/raster/scene-view-parts/4_water_distant.png new file mode 100644 index 0000000000000000000000000000000000000000..929aa8adf51be9082a695b1bfc372cae5993d812 GIT binary patch literal 668 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmSQK*5Dp-y;YjHK@;M7UB8!3Q zuY)k7lg8`{prB-lYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&3=B+?o-U3d z8I5me*ydk$U~ss9vvC>ArE8&8t|coX7&;qUSzC5(++dKvY538f?>}GIHerJghRPc^ zDzG^uB`|0iWX+l+=E(Wc@W@iJwRf#@Yg~4z-<=RFvUmFCwbtCpJdbZx^jd%x45tDljvfF&bab@$ZhsSbB`e158AaMmGeN%Fk6S0mZvvQ^FBXov+fTODvu zaMk`Pt%}9T<%PY%Dy`G+^=9t4!*FKU<0=dTjdI=xPtL1$gr|9`(3-mjh@*|tY? z1-oy)(q^XKRV}9`iGQftIy)yV?4g&$(Ynx}RE;XGScYjg_MO?stlt literal 0 HcmV?d00001 diff --git a/files/opencs/raster/scene-view-parts/5_terrain_distant.png b/files/opencs/raster/scene-view-parts/5_terrain_distant.png new file mode 100644 index 0000000000000000000000000000000000000000..7c3bdac24367b09b1ac0afa25536ffb827ca6868 GIT binary patch literal 1152 zcmV-`1b_R9P)o#DTy)KJ^-L$^ zCPkoLDCnv>ea`!Q=Ty;EEi>a~mMh!9^Qr}$AOW;(yQB$W`tB0W@9;a+Hb%)L*-5w4%)88+39fmwZ~BAb zPX%KR=;qrI;6{Hi^o8?pJLwC9OSLFSms<6ucC)6yX|A3btdHV!JDDVhI*m@NQA6_~ z0O+%iF4^31i3M>Bfwq&0t!>6z-t2Gl_M@E6d2eSVWfBufX5Majv#Y?X&gpBRQ-?bB zW~WtmVetI-5MBN7qCvs5$Uke}{&Z*2Dy)wZ)<%gPJKR>I9?ivUY>oZHjW`YhWk;5p z-FCC~+@^~Py*I#i?G=3IlQq{7%Yo+My6rk^n~7ZD0Au>5W(wPF*Il<=Z@*C22LWaV zr>j@!fsDB$%Hw7M2xvDW9;|Kg%ApoP4zdV#S6bA{X938(pBowF1{W2QEI*|Jz%;ho zYOooPx$|I?*N%3O+#_M9$?T~|bs@lR2WBHPS7vIuP+@w;4#0uYX+|VFDfic7T8)Ti zJzNO;tfM;fG}{Zk_p_vhLduioxeg|U1FI;(kxrd>oU%4b2waohJI!1t_mV#SsP3;@ zxCZk1FWgj%t6yBj`636q*QkX6++UBa7P?8j7S@!9OTx(!1j1=HDo;A9`@0v8l1Ur| zt{FPVbZiTz_j2xz%xsOo@MdJ*FmGhuCf;mphBplFO=i{_PqN!PslOp;=3iU_dWIb} zqbrd~+6ko0Ik~{OnhR79oD?`m;6xa>J;43l3n!`nD$V>wGv1N#piIJtPAw-znc24Y zmU^=Y!`bu~YyfzPd!XB3f0}UWV zFgRgmMetLp!Ol2rketqP!QRRCqH0w`M*K-sDQ%2owXw*CV?f~yJq S!rYDk0000P0q|pihlbKu=oM-AkA<{ZdG@6vq{w zAaLvULEzS{ea__XzeoQM5Z`^rA1(*G8K06Cao5Mc;L6 zORjNQ*LMLvtm})u^TSv3`EuFT`0A2l&*uw``@6d}B!GzXHv4H?u(#8}(&va*wHN0r zhx8(HHM*Z(@_AYsr{1qRpXb0hZQs^*s?KmDbl(B3D{T!{B^hD^;}C;dtdg#bH9OvS zO0H2@?Bj$B3w_DzG9hk6bxAynZL{KKfe;P#JAg}hVc_2$;KI6zT}LZR5`Qohq>iu3 zVHM~L5V0fZ{BWt!i@E{Yh2=gc_KB?{;XQ|jdZO$)x>@?XIKCEB^aSm|)muC5yWBG01F8!QKWOa1IszDWYmjLfE(x@(k%&A6MTXS z4T-y_LZvP^x<7}1+P=5~2c~0vhv}d&T>^u^vXVrNvZE%M)~yp^(M6{s8DW9lCA14s zEBOT}HDhJE5@3H?JV>6PRpvL5OWUYyW1wTCa7L+d;xS>m7R@XmQ%1-vI6!T7y2hXa>sj6gO7?zRNYa7x`-DWjQiimD2D z!b{JF%2Mn(c=*r5Q(JR>cYWWn9!W+ByOUX2R2Is7h{3|x7`a}cIU_N{WMt0`m^R}( z(?Oa>_Li4+D7E|dvUv{ku4~{omaU9PqiOl_hK-OWeOtJd_*8^MB^s4d%eyfX)38a3 zaUdOW zKX{JvHhX)PX9$(9F_Md7jm`chyk>9ZEhLqicS=?{z9f_y8mnn)W?+A7H`skIQO_Qg zy=MStOt|7t)bcLXI?RHw#*5N4;k6NjSY!`s%$>OvJu#m~tK)wn8wI zYg^#Cvoh*`MOoWbPyylX2&PdkVfH7$5e~nLJhL>k)1)EjdN-Fa& zJ2)$w$!&;N%pLEY2wwTzuiUci^myFSvJ_uNXnUb~@srMZFgew=!X*MPyApIPOc0kN z=u2GwH{f?#>+NtC(A&0~?Fk5IrP*HB8a|m8_?f!q&~ldpo*w4{T9yM~osPOW4V^yy zzDvG-U*24f!R)K6mA<=E()~j|)E!a)?ed?0K8*q2eETf`KmGYn2hiXA{b4?@z}H{@ z<0Yb<86O@VUX6pt$Hxg~dBt(R{`!~0`@jGCczFNYFTWVz=YRb?`}A5?_>$Tef4TcX r;MVPfz^&T{fm^o^0=I5oJc0iLzX}rIvR4Tj00000NkvXXu0mjf>gV8t literal 0 HcmV?d00001 diff --git a/files/opencs/raster/scene-view-parts/7_terrain_back.png b/files/opencs/raster/scene-view-parts/7_terrain_back.png new file mode 100644 index 0000000000000000000000000000000000000000..e5d3e6174ac9577954f2b799037e82dc60423b35 GIT binary patch literal 1567 zcmV+)2H^RLP)cy(Sy?$%=jY- zFOqoAU9X>=Uz|VXJujK@I{-fKt~bukFMi2aUpf7Ygr7_LJ%D=vo;Ty?2;3L&841rx zni%NoHFcw@d#1NerKuZLiF?jBE0LLfi5yuKFn3x4~XU)9Z`@kQOvL!eb5OhYh> z!4QKXn86IFn-EGET(R{2PR1PJ+z(snuKiw6I?S~`Izf+mwurIVv-TEzeiW?a-QKD=69CHJY3 z^~r2BKAw)P;4UxuBms-2#j5>J|d|+z~Va_f96byjtLT(O^6ZWs;3axvRNBfCQn^N01tLTVUx21@>dPPT)4l zXa^w-+LJ0^zH9`D!FE(;ZgJ>k!2bS#5V_OanMfwrk?9G1oMbve+8~6|7tP=<&KK=M zQV7O62F^&rh?R!d2n{1pKyh%r;q6QYV0L_o2oO6!_ym&Kj8I3Sd+@jl6Eh+SmAl}{ z;S$^hWA>sEsDi>3jT;Q(R?h?W5wvz*)=ZAxg9 zqiqvz>V#m#C|dD6L9Ix1;0Db>_NYIT>yLBUw|Mp**M9oNeiv!e-O2m|mLI0q4M7u+54t}1ysgt~5!+;Kdqio-cr zw6Fyn8|}4)?Cktv{_v$Q|E;VU5OPwI2078n(ExC@Me`%Xq34;$_S!Ftm6e^3!(i?J zt*FWoVnDM<8Jq~$wsc+D36g4;ZpyW}5Vy7xCM0$8E8x3|(s@`Q+=bC7=6fvR?;?i8uk{#Ifu4B74C>Yd@C6KOfv==MHO`rBuO)NjljGg2rTh1Q7YRmi8PG? zv~nhv?WLLI9u!E4t;~X?1el2>3rv(m~}U&3=E9FJzX3_ zG8*6BJeV!)D02Mc{n;~p&9Y}L3v^oTBt+k&Mr>7=%%q*N5d`UWDJHrG$h9%q# zFPR*6;bS(Oe*3*YJM`ZB{6$*l+2g*j>pQ*?z2bRNLgR-g@7I&Z&F4xQ|2ysIvbAA* zbNX!FuL0Sv$*-p~Tv!tc*P_uZ)3e<*YxwZ zX!vENylLwGI7e16*HpN8|Ltr3dII}j?UlA$m6qn}zo5M2*MUl#x7&Old56wB|0k)` zS1@CF@}!q{^mC7~G8~wmuykLI-J81j^gPvudlx^<`~1&x;eZM;*3=XUOGK<6yY~*CYBft2gTmPz9>(De4x1eV ze+#9}v*!L&#U#xuyEe{n+0%6r58gccVAyzTV&kj4nQdwp65ecbWz09$dy{`=exlTt znE`843XHEEdv@<&a`jx>kYAEhHU4;ch%;PbZ72zs`yg!PeLhRqdzB(EtuT1H`njxg HN@xNAZN3wy literal 0 HcmV?d00001 diff --git a/files/opencs/raster/scene-view-parts/9_fog_back.png b/files/opencs/raster/scene-view-parts/9_fog_back.png new file mode 100644 index 0000000000000000000000000000000000000000..c98b7ef29c06f7e7bd463444deb355fdd7878e19 GIT binary patch literal 1426 zcmV;D1#S9?P)9uyQ?x-WSz^{3$VoE4ARNX`Rne=h>D1+(=FTlohz>0z+bH!fNR$cz_sfJ z;M#QqaP7JQxOUwDT)S=nu3a|(*RC6YYu63HwX47HfnRcPHnM|K@(k#~ky2Pl6E>I< zllBf*NwW#SZX5)y+n!)Vc6nr@bunkNHwek+k%Vx_czfch;a}$m{Ca=* zrb2+|6Wjz6N+u{UsU}FQCY(5s9N3_cNSGA+LIO}$>t0Tq#sE?0VA~?9KKu zx&tUgI58M(f#kFoEMvlwoW%MlY;576nVuGqy}6O@gVqJI5lC4;gn|UANGecL&9~Me zLI%7(+tT758wwhAP{AN;>WqxmsWMJ!!KxnuOolC4!Z?}WAZ&07o8~U7Wf3N420{>W zB7rPP)ij2>T)2)Y-eosd_p*d8oxgM2)dd?HLs(3V>V7FI4U6o#~I6Om~g z;BMRy%0xsclCY42#nWo~0+Q;|AV+bDEF{%^;#QQh46$$sYyx8w0_x@^GX*3XayN2S*iB7u3oIT!bT|kIG0dtsN`)S2%PPP9>x+5-?0Ng*%QhJ&=3v^K_=d1WpaK4$&D{X=tmPJHFM0l0SE g09?Cn0IprX0cADL*{*E3ApigX07*qoM6N<$f?i#kc>n+a literal 0 HcmV?d00001 diff --git a/files/opencs/raster/scene-view-parts/README b/files/opencs/raster/scene-view-parts/README new file mode 100644 index 0000000000..3fb58ffde9 --- /dev/null +++ b/files/opencs/raster/scene-view-parts/README @@ -0,0 +1,10 @@ +This directory contains the 15 layers exported from the SVG in scalable/, +as well as a BASH+imagemagick script to composite them into the final 32 icons. + +If you edit the SVG (Inkscape is strongly recommended for editing it), export +the layers into raster overwriting these files and run the script to assemble +the final icons. + +Enjoy my icons! +-- +Sergey "Shnatsel" Davidoff \ No newline at end of file diff --git a/files/opencs/raster/scene-view-parts/composite_the_32_icons.sh b/files/opencs/raster/scene-view-parts/composite_the_32_icons.sh new file mode 100755 index 0000000000..4febc3b96e --- /dev/null +++ b/files/opencs/raster/scene-view-parts/composite_the_32_icons.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Run this from the "parts" directory to composite the final 32 icons +# Also creates status (masked) variants of the icons while at it + +set -e + +mkdir -p composited +mkdir -p status +i=0 + +for terrain in '' '*_terrain_*'; do +for fog in '' '*_fog_*'; do +for water in '' '*_water_*'; do +for pathgrid in '' '*_pathgrid*'; do +for references in '' '*_bridge*'; do + files=$(echo $water $terrain $references $fog $pathgrid | tr ' ' '\n' | sort -n | tr '\n' ' ') + convert *sky* $files -layers flatten composited/$i.png + convert *sky* $files *mask* -layers flatten status/$i.png + i=$((i+1)) +done;done;done;done;done \ No newline at end of file diff --git a/files/opencs/raster/scene-view-parts/mask.png b/files/opencs/raster/scene-view-parts/mask.png new file mode 100644 index 0000000000000000000000000000000000000000..e075fe8f5beefb0405be91ff15fd36ccb1edb32d GIT binary patch literal 3419 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Sergey "Shnatsel" Davidoff <shnatsel@gmail.com> + + + + + Sergey "Shnatsel" Davidoff <shnatsel@gmail.com> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/opencs/scene-view-fog.png b/files/opencs/scene-view-fog.png new file mode 100644 index 0000000000000000000000000000000000000000..0ba2e69cf731c2bea52363fdf76b62ff953dc8f9 GIT binary patch literal 962 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCwj^(N7l!{JxM1({$v_d#0*}aI zppNSx%;=;syMlp%>9(hfV@Srmw-Jt$9xL#?VSC#D`&8}yxU?V*qw|hg~Haa>lVQ|TtcVNHTX0Z#d-iI0(cE5VBe&yD)2bC?3Ngh(N zqUnlHclnjvJE+k9S+wdB$CB;g4q}aQD{Y@Ptm~6%6hD=b9WJgs|U&mT6k`--l1-j8NtIGjm?(Jek91 zxpjkDm>YBHuK1l>!adhITu5DQwQ=DG@yOIU5!susdOh0QI)QERbEjOl%=uB_PJSWY z*k<(2AtNNo3!WXfwUQ4b9JR3%liH7yC~AsK56%fSy@jm@71k-X}OpGudjZx z@!GXAL0gYU`oCQ5e^7_fhH>de6`_b{D^%H+zVnlmUYl69I5&HDn$9~u^D{z=wMO?y?OHYCm*@3B~=Ap{r$x5o0!M&=WWfW8#}q9rhiO7 z^Cf+3m%aZxJen2I77`Z>Bq1 zK6_|yq{FJ{e6r8#@dEV+C8zH_FXh~PMBq-y@m=%JC0^y+Jh@zKsg>-t<%XuI=7ld+ zJ66rVzWU0WqbFo%9>0HeI z=W1`(Q4%_SK%a9@w`9JL`Q8i4+j66qtAFg=z3Jm_kF%%N`<_;2yRN~bGS&a>-U{Ix zWfhEnZp@wF&M@!$lLtb(cRmpnlvMjDcj@PXo8BsI{(ry!aekoip<}l24HErERK(!v>gTe~DWM4fQk$Z} literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-pathgrid.png b/files/opencs/scene-view-pathgrid.png new file mode 100644 index 0000000000000000000000000000000000000000..6586b882e6c0473ce28aef884427a6559f35ea50 GIT binary patch literal 640 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sw*!1aT*KD=3|ao&A^KIwvhRtT zU-X_@H1W*r$!BKwpP1ttcEL>l(S#GTC!d`;1%zfyI5QV0*mH7e+@{w_Tb}iwo}at( zTGWQmkwCQmvvc%|sP!Or^oCCbyD#*gUYM}?Rp`nePVuipmi6;h@SR8*iYRGW<*ezM1qwVG6YPv2{O3}oTv(T?l_UnvA zCpJy|v&w&F<}st2GD^=r^+bwk@8CXN?P+#6Vq?g^DPgNj8>$xHd^BhK#j7u^GWvH# zANRgBXY#)TQyAKmB_5}EZM|8u@k{NUQ*rsbN_IOJNWI}dKDm%j!u{?(nf2yh^d8K$ zo7c$fGxN*4D~C>hJwIQB<=}B?foP$Z#|m3|fc{Y}ag8WRNi0dVN-jzTQVd20h9gcyqV(DCY@~pS7(8A5T-G@yGywpO@+MRO literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-references.png b/files/opencs/scene-view-references.png new file mode 100644 index 0000000000000000000000000000000000000000..aa3bc73b2117df07ad487cffccd165e25e7f7794 GIT binary patch literal 1785 zcmb7_X*3&%7RMuENr%4B77bI>GHQ(=mX3X=wUtU+VlSyBgpAmNR*`m;mbR9}QnW#Z zQifNdYK@(0GqpvjeTiKWHR*Yu-uw2>J^%Z^=brmJ_uNmH=wN3lcnW$7000PDTbVg= z9QITEyqxR`JV@afueYtG8Q|n+6w+R0aysBps~h1Qg7H&aCJK3Y&XlmVnXz-g)LJeR zBjI|k%Wz>J`dFrp){PdAIhLv4ZgX*lnv9mK2=bh5$7~9>Ynaaog2BY*!ruWi;C9Ib z1&ozcVp4{p!zBtu%4DRWq%yOC%CK$RCd{0 z3Wv)8`Ig$i%XLLFsTU|S&W<=8m)CVo?)dv7wvl3Nb~*$t@@EMx=h{hJJ>mE&_K!@N z+DtJY6X0-Fsx^edCplv;sp4Qll19n_S>>`4^Z-?ab4#RXVQ(EcK93K z$syu@$OT1-kE$c|23nWG=9$giWNagt-%4VE^6c!@dwn;pjh4q<@>cf*cEWe21w@~r z77pznC5qKFf(^DNSec-=P4}dy+s|~VcS4Z82Y-oVLkBf-1}%#*F!=`w>P1;sB2bmu zX0K2tb;GCSW=N*RZD<80mz0Vzovs;dqPChEHX`KoV>IWw=)Kqq3I4<^B?FAIo9}Jx zlQQCF#Kv%x6N*>ei#mGdHQ(asC#!~NdJ>~;4@zw0t`(E%Q&Zkp{A!w=%-^9~ecW3G z2|72MYZ%)oT^GkvJ{Kl?W8iEtw2i%D@|>LbWrJxEU2= zAx2m)y$NEESCH^*ozC8j%@4x@m+#LU^4eCcP~LwfI&Kvo!Iq^h9JPX2SxC2Lo3J=C zd$CO-Ki?Z4pm0gr#b$h5S%ix9tLrw6vDA4Re|KUdQ~>32!~NFm)>AE#Vy(}=;D333M4ADW{GYXX5W_OQ@p^N?S4I+z1A`e7$8 zq8b_4SQDKVUo~M=5XHsPF!c2*{?JqB4NSLaNwUh+g@BBeMF+Pb=dM$k7ms!#mvW*N zH4jUQHhR$V+eZ+gXQvp8`iHo~e7u3cTDO2sZ|R&NcD1{;fpyRUj4L?i`jX@Cec2fK z?;WLNzWu^-<*LcT{UufgIZh;@*}tW$rvT--`ju9+^SZ;LMp!5m2ZEaxv*Ui4;PdHQ z-+a=gy#Kxw{3MT{$UCBOpz~Kf#<9$K<4?;lpL=|E1g*zNS~}T#w@(efGxP&#z+prHCOi0pfrhAw zC(R$|@5caKicjBt%a1?5^lOk-ny{LY@igPvdRA3{@pQ^K7p{NplR>N`z?Fyb0@Q5Z zd)*}nqPaVnYhG!^sUJ)g45o-Q?{i%39vgpUhR>>S@cG7$$lN)k#Y z@qE!IGR_Q~lVGrn)Wu+MsytZ11_Cg#>)|DmTd;{~IZWrktIhL}V1qvP&Z!U9JpXFoQ}e!SRji^P+*{%>2j4WgAV2 zLIbMG$+xyvjjnm1n6L#^)%%UpevhwUghQ5FTu3}C#dlhJrBGY)K|FN^7cVUP<<|2{ zQr>Qs)(ap4mXs!IH{atcpU(QVbq}H3gKfnxKqy87r&{Nm^ z53onSY|jDC|J-m5b2}o^3+o4njEq!8hXjWEc!m0@VzB;s>%T)eLjY@YJ2R?@=iPq* DS?Wvm literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-0.png b/files/opencs/scene-view-status-0.png new file mode 100644 index 0000000000000000000000000000000000000000..b98230f01319d645f80470da546f7716b00fde04 GIT binary patch literal 2029 zcmVT-07~{bh14EC`~Ncf zkpS#b3`lwacE!NcI3@q2tDe06e~Uu@RO&+?`CdBbK;)v#%u|g2 z*KY3^uXgoQ1Hjy67(an3j3c#mq(&BW*sS`7xXh|qAisQvv41zX{i3vKSV)yJ|Z{f0R~(_3?*UIH`XWG5l+o42TWF=pIhs+CxgE2;h}hZi6Hm()FPTVQBKRH!ksD?HHCNeC6WnSVq(~Jy{zDCX4fj@jbh% zp$Z`u1tRGH_`&z)Kqsd6n67Eozex}r2ViCSIIn;A%VZ1R8Swm>v!k4QF=lb@C>!f8 zTbrABp38x$DTYd=bO2oX`WXFO2jO?Wxybw1J~Ob+_dm^5~cac4~&GrzRVn{PhgwbzP#{?$Cc zefuJRe*eYpv(rf>Y`belw=or-tO^!@CCGY4B*(z3;^$4zma&|wD@rTZyY`PY+3*UM1*Rs#{A+E zD{E^^jE!<^c9!8%sq1|aVeS5X=9iX;V`27V(@af_BM7t8(|r8kGc;Nr+dB=GR@SIg zcG3Y51U?VfH@Nzj>*R`M#wHKba_ju@>NU=teSz}uFe1Y0{d?S+Utpws07DzhE#JZS zeMUw`SYF$tQfs1>!m&)o%4NbZOlb##fE%~w7@qzVPNqm$zl)VAQ7lF*EH6Fr0stc;liazt$?}65#hlH-@e;OWpp`-?P)el(!1sNcjVcqTp2xH-nwf(H zwcBXlhJIm+7CwfQA;ld08G=w_X8g7G!qZ1Q;Ppgj8`zv$O&HOs!si{z-mLGQes(q4XGY*~?;0F;pJ4t5z zEVYg66tX6^p}VCxp+^~MU*;vv7}(t$+oYJ!AP7T+3|rfETra>FIzi4FqW<7dWXzDP zV`0#f%%m^|mJyTBsyXKxe1OA9@vU{aq@xN8~aV zS;s_my)L~PXiS925D;zcu-s!`Mh3r4&&l*p_DfQH{z@6A(mebm0iu+ze5DncB`xnr@S! zyn}5e6+4CE9~M*9p}6X9{Qv*}C3HntbYx+4WjbSWWnpw>05UK!G%YbPEiy7xGBP?a zH###nD=;%UFfjeHK4|~|03~!qSaf7zbY(hiZ)9m^c>ppnF*GePF)cDOR5CI;Ff%$a zG%GMOIxsNy$vaN~000?uMObuGZ)S9NVRB^vcXxL#X>MzCV_|S*E^l&Yo9;Xs00000 LNkvXXu0mjfiZ!`T literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-1.png b/files/opencs/scene-view-status-1.png new file mode 100644 index 0000000000000000000000000000000000000000..43b597c01e28d012d26e523cfae5397e0554d31e GIT binary patch literal 2787 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 literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-11.png b/files/opencs/scene-view-status-11.png new file mode 100644 index 0000000000000000000000000000000000000000..3028217ba9eee5a6ba778f58bbf1558cb9bc23aa GIT binary patch literal 3186 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 YWiD@WXPfRk8UO$Q07*qoM6N<$g2O2isQ>@~ literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-12.png b/files/opencs/scene-view-status-12.png new file mode 100644 index 0000000000000000000000000000000000000000..974519c5492035b8cb936602984064234b6cd5e2 GIT binary patch literal 3025 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 To9;Xs00000NkvXXu0mjfXm!QX literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-13.png b/files/opencs/scene-view-status-13.png new file mode 100644 index 0000000000000000000000000000000000000000..4c075704af5a59fc6a79634d564cbc6ea9aa9503 GIT binary patch literal 3279 zcmV;=3^4PFP)ba}aj^l2pjp>jCCys++nP4FhASC371zs68>{wVVSg-&D1QxIXVGt4`3PJ%K zJJ7M?7~JiIa>w*MRP|6p*YxVWdc%EpIETd@>b+N0ZJVgP;!3aXz2|)QJKymCzVm&j z#IJtSv+UyaOJ5u18?UVF8h_}cE3m6+RDBFFuv=?bKufXdV!H#J zeq0Ese!e>~v=4a|+tX|P5v}uoQRYKK=p!+YEw6Vhu-$D2fW{|Ea`rcy&($^2-^C8{4E!p+gVC*EKJciv^ z!q(>@Xk)_;a^It`qBwpr{Gzw+O2@(Nf?j5RL>2wAXK8 z@ezaj5pEf@gFyE2C58N!6|@#MLr*49Kjn75`v@L#9T(TxeoiY(r@4T)GK1Hu6LdVB zy~oLooghCvnb8H!RT^`bY23X?=rt(pJx2M!DYOKH+(uwQ5*K0z;Zm)|mZAPjD!uh< zW7w#zvc9rNvtC6?h2toc0B?4M`mJ|xD-)Cto~CyD0zqSyysH?{5N8Z|n?WmS zGdZ?|l<1Qxo9vc!yZPQrX_Y;OlHE(2od$D4P!hlPTr6l8M8^IIez}k-Jve3c35Y zuONk_n3GgW1DhdbAq-vKfA1+Cd;Cqb_Is=h-5MpllOd5=7{wP8Sq>vy4Qiw#M_y@%IuJ^1ExOkVBervP` z>tOkh*I(e>3s3hb?fYNk^zk}h`to06EkvQ^+ppGm^E)0VK6Rhk^}RTvNWSE7@6H
x7}hKmFs&)T)o5WfF9L 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 XE^l&Yo9;Xs00000NkvXXu0mjfcD?@6 literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-15.png b/files/opencs/scene-view-status-15.png new file mode 100644 index 0000000000000000000000000000000000000000..3a2073628d3911d99cc2fd3836fd504bf3343fc6 GIT binary patch literal 3354 zcmV+#4dwEQP)lN72G^##?7}%;cETE-myV!05 zr=J%Bs+aF(4DGu-ijC>D{(#o`zbNy*A@qTm$ClTdrMOOB_TAaMw*v3V!EX8_I=I_S z=4mC<=M5I=kS>jN`z|z_4HsHWO*cN(P0`*dEFN2=DxPDwZV-aV97t@}o;!qUAN$}5XFN|A9S+lTs*qI|3a>J+e-@@4ogdZ=V0uIzA^(ev8MS{2JCm6k5K0+NaQ;;qZfo|M~HDM3hG< z!QzUfy!IU8H$OdXZ(ordc;FtN`TQBQV^~=%^XfnTkl|8=6OR^{zPg7XaFJ3Xgg}V& z+WyhH@3!-vzxfAl{n78XylZWoVX!esthHE^T&=-ci!t#vSFAC9@DAVkb`>|HIQ6&- zRuF~`FaO&Yn4P&FG4b%N7KAd{vt)B$#F&VQU+m-9(LCRLxxs2V=&@58P`2}_dmBi; zJ%3@6zdv!Hd%x%y?aedc*F(2+RP*%HE@#fvxN+S>2!ZeYIbQRzcv`0d04Gnr%CA24 zBK4Z)rI#v zQW|5pJ~d4vnB$pe@@U8Kpa1bJKYFtbJ^lQbLI7&3^PK+b?=#Z>AQxWW$E7P*Ik0Ch zd&hTgEPQgG!YZW^QXo4NjDyo{323ZoD`7ez6TgqYx0Sg6&_S+E3jXDv<~ercK1PaX z$rrxNBM(i){ebJnjy(O(e})OGoc+-`06uc)Amd}Z5;RyuS~CfYuxb4iF9H%{3_^l5 z0wXL~gGm4qhGCo(ZR~}%wcEJw{-wNQa2(#Z4}do=UW%=*9e#HGGKUU5fL1a-mnbjK z@$rchNTty#wq|S^7b4E%i+Dd85`Y+v7fF}E5uixIQ_>?U@<24-awpzyY?EN-gVKYni!wm+Mng7-KoGXPnXPBUmg4 z#>d$^wu@S$$;w)dsoOKGuCAfu?L3Vm5pNR4w9@A6!aUP=R~S0>J6H=>FMpk;@3Vi; zIKoJ(^$OoVcY&cpPce4r7pN~>;>_98e0<^rrBXitLEv+5et~m8zDT}rkl`KsX?Rs$ zKX;yqyfeGV^jw9$yvx`~3D;3cvLKaJpoE}*U^{QT^;3R$@h6=B$z`G_B#I)|)>hc} zkrNaOF2^4p<(Hp0MXgrG_kF@3WNu-B;r*wGjKvyg`iA`OQ_RlI5(IJj^?jdutxW%p zhbasn!5RK2HnS7y6u7srNEn2KVMw`LA(P2tjKM@9)tZOYS#}NgG1#9^D$gWN(k$d8 z5cS@MF z15b!pXr&arbYU1^jfo2?pc%yT>R!933lk57VTiRB-;W_$D(3LK08=^!oQsW-u5S#h z^%`Iq=+DPOCs`@&S+H?@T0lFh=X5l#RWZf@286(oN#oF|KoDV6?4clvbH1wunT*2o zLO`@2lq#aN#`go3*XpswMO>l>VbE%P%hfuOF$|aTT{}HObRC@7R%4@N>X`ViS>G2r zO5kYORV##1fHbLV;}3pl@x!q7ADJ+LP}qV}h>((uQq;X>OQ0|asCxmK{16Bj?8_!- zXg5i8;9dYWNzvMo>8n&T+0~DTQ~|kp*Di&in9~R$s5e5QQC!Np*>Y1Y*G|5Z>`K%`EN~*JpO05n+t>K}RZtcu5U51S7P_8!c zMov<@y@(U~xQ=Q?>N?2SwCYVDFNjNtu3>H5eRT{i1u31ipoTt%R4&D07NG{n6kW>o zIQ!JSkb1)sgofdPcta}m^|Nzl2G`Lnuh#II0kwutwI0WTP%il$zeIiZzmQVIrA$Z4 zv>hE<+SjGk_j(03IO|n{RxW2VTGYgkd)Do%W8Z-?1q~kvefY| zZG3Xs49#YMA4JI94zeT1sVrQinA5mQwwesvaSCFSWn zW`3nxxRfUADvEiH07?bLimQ`Kb^XY=n!Z97i>1GqWoe~~*9=hoN60(S;%#Bjm{YjehXIZT*lg~QjGP-?G znE0c0UV3w8ZC&CwCL#(#=2vQ*y|}>C-H;?BlFw;|N)7@r#^Sh&rB$DDwVj1rSCW?L zpZMaFeB=4QjiD$dT1%Q8aB>}uQUcEl2&1Uo515FkHbrygZ3t@=b1qp|Ck0%)|JLhV z0Nh*gIdguN$r-PmhZC}f7jnnj7)G&jE>Z|0V{x4B6c-so0000PbVXQnQ*UN;cVTj6 k0C#tHE@^ISb7Ns}WiD@WXPfRk8UO$Q07*qoM6N<$f?W1>VgLXD literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-16.png b/files/opencs/scene-view-status-16.png new file mode 100644 index 0000000000000000000000000000000000000000..d0fb5404766f61cf3c760960a2a0ce621ad4633c GIT binary patch literal 3078 zcmV+h4EghkP)UL_t(&-tCx6j3w7u$A9NhulwlN zO!s&)o-m&Aj7=On@gf!?iXaIhvLK2A!n?@=iIv5Y1q;L?3xwFP2n#kKEQEx3EfPVD zgTWF(u>-NK2v0KOneOpS&$D~__I=j#d@QPN-MY7X{D_!{kvLN6-cxm|>i_?~|Mx%V zlzQRs6X5~27ru6czkO-vfj!?oNbu99qxHYIe=OgPTB08Zz1=#P+=E+@-vu?Cqe1MqkiWC zy0bz0%GX&AVYL$i;h|%rz#|Gg0J<$p*1t@?_byf|JOj$pK*XeX86ib2eGKJyzyh&j5uH{*&Lf_D}8r!wR{Z01X+B{eTMk_jzf8d2D)|coHV+O;R zyl@mwhyqKu9n?~pHs=HwF8#g0sF(4-Y{lkrA>ImVC9shWV)H>d_$HN8_UOed;Aw;I0G4(JVHv?(JhGEepkUw9X!^pHA+BILQ{ zuU~qRTia*xJmELae*@1H)tkz(d1kqC=`qs8uRT;DI8h)nB`^b6zO<18CCr^s)wtOJ z6wr=5c6%d^F1IihktxqV`J;=Vo6iBzjEBEVmYoCO+SWO~@rS=o)cJ$C$UpP!5uW?3 zN(2o$;S z?SI*7>iPMjFM}Bt&)2YK?QkDAR~5FxJCLQqRPBo*^Ri z2Lmo$+2q>xHXBEeaQ5_RR+g9R=!*#3H*a!ja}(!;(;q*{@r`u^;q=Lq{LG1uF`6Xo z?T^^JwoR|MkInJ|Ycy$AFqn)e3WskEOYH#PvvZPJ%+Qp?bnXarfwqc5a-3*fQEnx( zEamo{yS)B`H)(a(IC|_cCh?GefBg-fd-l_;t*js-T)%mP3zsgly7mZ08!m2LB~4RS zS6A8E-k~=rO(XCuN7vTK^BjBp=u#uPLY5co4`X)sCj_2mr5hcP%we*!M&ST!3|4Ej zR_JP9k!KliU%1H1$)6_(yX3?7@WN%f-Ga+on=C*5NkoLLYu8v_Stp7*0IaSa6U)bO&Ve|FqZn;Nj;(d@ ztSK`+t-)%Eqi2%}mD`k{l!A@bHokA!?N4ykeBguwMOPMZxeF_SYm`=~s-V*}DIH%a z#^VH07SlP03XUKmsNLtULH?8{FKm=Agk6C`YmRO$WWvw|!Z_o#X)ujkf z4f~b_0|C8wqLxQP<15_=1?-QK%CpOC#A!j{gmxI9D)1%ngEdgvpQR`q_x8t(;uKM( zi)U2^K+zwL$@7A>Zd3!B%|*?lnSq5mU?EkAu+l9vu|G~pb4Ok{lDr^_0<@_B*T5A@ zm4r$HX(go`r8S}NHT=FL*dLAwT1P;^S~sjG)tR)L_-2x~HA9)NfjLQ4vuh{xu_k8} z=NPSs0w3E1Ujx_mt_&!PvhrKsYQzbiH6&@7ncI64?)1jA0+0&oyXxbP7UKslN1)w5vxzV z$i3_+zSa0v&tZ_mFf7!lZc0ZM>);Sc70wZ7MOn0!RW^%OYdr@ZU^Iy_#@72}trgvN z$og8BRumGfe}+^-vfLf6f<_I{yhc?6Ft7JHNj3CWIL9!~hyoAK+S)4L4?IA-yToIs z9_9GPGGX8WiqRxxw>Kos3Y76_9s4=PNkLw?gLOC;tf>0@ZE$nt{gH+#fMMv@Mz zNJ=&Hm*LjYaoUf5iR8|A=?(7D3O%C0qUxy2>>H^9yXg5MLXzbSCmABu65F#2R@rnM z%?Gs~U%3r{(YoH~&$8iRj3SOR@}elAc^$&zzeF+GWU}`Tlm3WK0000PbVXQnQ*UN;cVTj60C#tHE@^ISb7Ns}WiD@W UXPfRk8UO$Q07*qoM6N<$f}ODH_y7O^ literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-17.png b/files/opencs/scene-view-status-17.png new file mode 100644 index 0000000000000000000000000000000000000000..9d95cc85536e198ba0076bb3b793bbb4ddd59748 GIT binary patch literal 3265 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 literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-18.png b/files/opencs/scene-view-status-18.png new file mode 100644 index 0000000000000000000000000000000000000000..0a9013aed32788b93054a531b5560b4f3ebc2287 GIT binary patch literal 3201 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 literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-19.png b/files/opencs/scene-view-status-19.png new file mode 100644 index 0000000000000000000000000000000000000000..d57fab4465943c7121dca49c833fbc7d9dfbb120 GIT binary patch literal 3327 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 S?mQX*0000)zYl6OxFD7>R0?y1%c!syhF3{^x(r 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 literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-22.png b/files/opencs/scene-view-status-22.png new file mode 100644 index 0000000000000000000000000000000000000000..afb0c7dd5855d79befd867a96748b69c0150e07c GIT binary patch literal 3389 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 To9;Xs00000NkvXXu0mjfUutnA literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-23.png b/files/opencs/scene-view-status-23.png new file mode 100644 index 0000000000000000000000000000000000000000..bee128d24d554f51c6b8024e9bae0a574d040a8f GIT binary patch literal 3464 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 literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-25.png b/files/opencs/scene-view-status-25.png new file mode 100644 index 0000000000000000000000000000000000000000..151278eff6d41440c7543a54ac6bdbfc6cd6bc5b GIT binary patch literal 3355 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 Ef}@6hbpQYW literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-28.png b/files/opencs/scene-view-status-28.png new file mode 100644 index 0000000000000000000000000000000000000000..5008026a158aa85d88540122261ef728b29efb1f GIT binary patch literal 3388 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 P00000NkvXXu0mjf2hFzO literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-31.png b/files/opencs/scene-view-status-31.png new file mode 100644 index 0000000000000000000000000000000000000000..3f8be3ac17fd5ed55683a52356924b4d2415920e GIT binary patch literal 3553 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 bV_|S*E^l&Yo9;Xs00000NkvXXu0mjfBKWxG literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-4.png b/files/opencs/scene-view-status-4.png new file mode 100644 index 0000000000000000000000000000000000000000..f29445f5ab90cc4909ba75453a1cbd581ff54ec3 GIT binary patch literal 2708 zcmV;F3TyR=P)Ss4VJi4so!wxz4M*>o$s7`s@1c<4}^O-p8f6! zzk6}zo;}~Zm&SL7lH~pdpt*O8ydwY%mEPX~l-%b4?t2R9`<+6X`y7Ch`b})c^GwC0pSPmvZjs>SI}y3VkS)1OZW_i6jYDsf1bZ z?ixVQZWBi#%U9+JgEpRHFg8@e(7J1o07^D1j!-W4fM+*0ML=F4n)TUv(jX)W!d|2Y zQTJH67L{WMFUsDfQCql^r3)M!%sa5|gpN7SZf89Mo?Z3B4XiHjhahID->LL8g?=Edi^ws;uJ z629}?3s^>yo1V>$Ba=(h4-p1-S3?y-Dhfm}yhpl&+5twLEy7%a*}~c zrGF47zK;YE?ffCS;M^5}=f5|~X0C(qmp?tnyYGB)%e*lB0w)jF_|lhui3lXI@bVuw zc;i){!^bbPF*S~*3b;j!h50pV8(;4apmpUso|7e$wx8mR4&4=i*>j)Hj|7oSVIG?N zC;Pwg0V0B=lKqtw5#hp_lZXgjsmWKrc9~aRZgAnO!4pregH(xQi$DDK2W+e#>klBR zHwgV+1?i5D-W7mXUjHko{xwhcs>(QhOXpq-l!epHw6}mXx$JCrz22o#UmKuJOz>Wxn!M zkw3rs9PhpRc=y?Tt+IwIw6Cpo&l__K3_g4WrS&)5A>!!tp=?^({=;2$J5~Y26aE2<> zr{|F{hC~RL)(Mx_!O$qnKwBo-wlQ1>v*2M3l+dQt2S9{~P^;IOzBJ3i;v(atBOE?( zfWbwOVn@#0>RQ6q)>DXQW4tih@>5%3?HvxvTBS|2RXT zJk03CL$v({e|!5KPM>;=;lV*fgv(d2aDIA*q2XN^+F)vKo-hm<8X975afNEVg;om3 zG8r8nCXVCV7jsG}O8fWW?HZ>(Gf%V82FZXVso;kx>-9X*845=P)%Ni>ngpE{h$7CO zpJH(D=WyIIapNN_w?es`Ff%tx<-{Y12y+VyR0hZJyb=IILlewjSz+#4opQlu_gDqn zGSEt)RA$>JmKMWx89aWJs9vWzzeuy`cY!Dcjdp@U5yYAN7933!XB-a0kXEzC`0+K}wW#00d-3jH1;F5M^SNj-Z?oM1(kwh@%Jr zBu$VcVRgNVRxmMK#L%i6+>%!roq*q-E&}ezFr~u}W{RN|lBOsWO+Td_Bp6yToac@> zju8>UFh(gDEPHs4jZrz#rC1!tK(^7T)@y(;G~fZMo4q?Fp5!RxZu>&c&>#&{Be9^A z!Z9_jYtZ%+!Z>BE5qAv)Fin#%j9A-fWgb-7>!Ud8ZhUKv7D<{iS}AmE{tjg}_lV^` z(N>W-D}6h)c@U=rjVuOiQ{%Zt4j{~_6r;4pF$`LM&^0KIB3gcgQy2jS!{to9MDi*; zKiS+~k-X%%D*&~Nvyh}{4JFS+D@DB(lcYizr-V6}G)*%vAjr$~>+5Zn zsx1nRg=?E#+9)MmkP6k?=4{%!Z5u#yVUcL920)U)M(*W)W4Z7&&i>P+*tEhZ}?${~n%V#KgCbprwtvI1a8Oa4h?lcCHAYkL%G-XT)%6B`5Mc})rC<%vxb}BkGsbf)40@88^#!)}Ej|%|V7*FXY7R*@ z?b0j*(@?a-B&&&{1i!k~V9Urt?C60Bv{poM!sV+~{2(F-8@VSlKC7%wKzoxE_dG?g z^d{B%8U@$Fb4*m%>(c9y-tPB-xY?pHw?Mqn>SFE$saA?46>M8`eJxeSaefwW^{L9a%BK_cXuvnZfkR6VQ^(GZ*pgw?mQX* O0000%el-s zb7safo}2CX8oNpCCQWeLL^LK;M1e+Ck;q$GsS*e&>KiXSARa*op+4{c5~5OtkSY*; zD5wpAl$56A($t;=UovBR#vaeup6j`veO(_9d!Ie$%pB8Fu)z{bI@)Wkz4qSU|NH*m zWvv+GE4Liar{DG)UP-va^Q zIxem=_?~SEe(N^g#v)$3PUw3$`;U>EI6;1FHd`0AHfgS0qIq+U$ZJyAf0WXJ(+|i8 zti7KRV`$Visczh%Ro_Ny3&*jr6nIM;)UUmRTbZVG@C>!#Apdi>r5a4l=Ma%p**w$Jh{>1 z0cn9~Hx_ST{D9aGc0An=v%bnZR8Aek%IEoc+pX)D(MnUyX(~hIohTv@MJ^v*_(h)h zfj+*vt+;|Ra;>Yw6RaVmPUHVSR7FB~E8-7JPG1S1MWQ1?0)+PZL& zT#8yACD2qy8+J-uq49_3!@+ zsKGT)HmlP2?OHcAZ}ZYiCEk3q#b5q;6{Qq`_xpH$QhsCr#BZXB_}r(TL2FGE$DF@- znMV%Ha%gro)xJjS)B;*3RMKg`JKkj-e*DM0@}*x-oHEn4!H8gtLDHWQN%~?i-8IIr zu(-r`zH^6{UoP`2FAnifZ@kI}??0CT+(n^85I+c+yL^SChYs<`!2^SZPy7A!(kPwy zN*7?_oNgwd5wiorB$P3!;9?}bzXRgb@ng(iSN!!~t#SI~qfC_Era1IGjE;`7w6a09(MBsp&aoLE8zYLMegIJvWqh_Q3nhvhcelwu|2qsFe2&Jo z8$@Upm%qWG*=dwgTwPoyo_Yb9c^NE)Q$B!^m@C(Q!qcZuChrYH<}S}Oa`0Jl`7%-S zCQiOWxg2wCX_3n5ClC>qmY1oFOi(D402m#eDS8x$=si7Gm!P8Nkk2!e#7l~MuE3o(_` zz`Kbux}P_OYP$spqr=5S=rk%RwNq8A6wEy~G>Jf95@HNEoQ=X5V)AO4AN5p z_a3iE8lwM4wv?7ul!k1yQZ(8Taja6yn4W7==eV{;jNsT7evnw^?skW@YMWxtA@AB5 zZL;La4rRIu?G6U|fZAPn4WLk9&(O+0Megt>o!FvQ5BiK)IPzuI3uC}{Suegs?(kU* zF72S3Z55(zMjwBTt@U?t?OV9EPBg8$pY1)FldSUV&a0or0!pbp0F*UEJNLxiwLqz8 zQ*L}zd&nw11Ab_9V_$HJ(Q7d)tmapptDgy z?pgYFt>KJ6i?&=!r98?SWi#in)rt|I(T-@egkoeEA59X{P3`5~=N zK(n3rfwEkRlb@lz`hB!kNh#A4GNosS*4@{2^|}YZz&{6d=R~MpUck*y5*b6S-eGlp zoA$ma+0X!`f{h+JlAm~r#`<|m1sm7W*(ReKLJ~_z zpGZGF3MEAmvF}s6w1DXZxC5TaJ2sUe8>L{RWYOBT(@J%3%egk?p*(^xT*|Y#-Nf@l ztl^Usone}Henj4mDC8WhU6koM$ev8xLvXZP;>VY-CYrKb{Cb<#wN)fe1=0@N^#+!u zu`CDBcwvlJ-T9x0Vr;QdtpjlK=p(*VG^D7zVa?xgL=6Mtl8yI&j?rsNc zHFly<-m%crpP@^dkFVbaz|tDq?mxKAxek`4@Vt;Hj+5^=CGyjsCvGj$*}g=l-l9}+ zcS3AWZWtJa!nQ|qX_>g**+Z#DJEGB!20cc(XfrvILn}pW1lR2sHa3PhNUS(i%rQPX zgi?w)7F05UK!G%YbPEiy7xGBP?aH###nD=;%UFfjeHK4|~|03~!q zSaf7zbY(hiZ)9m^c>ppnF*GePF)cDOR5CI;Ff%$aG%GMOIxsNy$vaN~000?uMObuG rZ)S9NVRB^vcXxL#X>MzCV_|S*E^l&Yo9;Xs00000NkvXXu0mjfwsf({ literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-6.png b/files/opencs/scene-view-status-6.png new file mode 100644 index 0000000000000000000000000000000000000000..a2f9a8a99f34a646fd586e622f6fe1ef4a0f3b45 GIT binary patch literal 2877 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 bV_|S*E^l&Yo9;Xs00000NkvXXu0mjfjqPFn literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-status-7.png b/files/opencs/scene-view-status-7.png new file mode 100644 index 0000000000000000000000000000000000000000..ce4e20e005db25bb5db28aa4b78e2e58328b208e GIT binary patch literal 3144 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 literal 0 HcmV?d00001 diff --git a/files/opencs/scene-view-water.png b/files/opencs/scene-view-water.png new file mode 100644 index 0000000000000000000000000000000000000000..0289b3c2cef68a5d43f770ff674ea4d4ee785675 GIT binary patch literal 1066 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBgK_U_24v6XF`S?q~4wPkytX`_6b0 zwB&eK!WXIv^ydQZIPTye?;>?)`76Mprled^8txq*#mol8&nPJiM*_a#t)TjiKnv6?rm2Co6ZI|p9AUwTHyk8 z&%}F9CC7snzjrD<;aqauzwUHo%Q>&gQ(_kZ8*|^RN!3}4Cg7KXf+*&UC z*PeC<>a9HsR2kI@3Uu$B8!1>ejS7voit`w03*85)5S5w<9Kp{1nc4iksc;D zwKTCsqMJ8uEG{T2EH3^au(EM_|NKTrh84~eRNM+S+(~^t{Y_}AYy}S4T*z{*^S%Fg#+eKak0b(rlyv8n_UcvHS+5tUU7v35uq8C~QOJD`)9ouIpUD|)7HsD`Cf2d2 z>zdh$n+9pz%GWfq*>^5pEWBvT>fOt?ua{2?tQJ{TuB#>@I@NcM2{7Sq*q0gW>N;1n zKv^wfK~=0nnpKd)p|6U@%TIGOOl4xQ+dtuhGz+sXFosl1Tq8?+%fw`5jxwe6^m4U(EO?H_m8glbfGSe#2H5gkNnm{ysJ8A6?)F276 tAviy+q&%@Gm7%=6TrV>(yEr+qAXP8FD1G)j8!4b722WQ%mvv4FO#qs>=IQ_d literal 0 HcmV?d00001 From c1e6b8608b619b9231adf2faf881b4da66fda45e Mon Sep 17 00:00:00 2001 From: scrawl Date: Sun, 23 Nov 2014 16:17:58 +0100 Subject: [PATCH 111/303] Always create a skeleton if there's an "ArrowBone" node (Fixes #2117) --- apps/openmw/mwrender/weaponanimation.cpp | 1 + components/nifogre/skeleton.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwrender/weaponanimation.cpp b/apps/openmw/mwrender/weaponanimation.cpp index a409e88073..c59a93feba 100644 --- a/apps/openmw/mwrender/weaponanimation.cpp +++ b/apps/openmw/mwrender/weaponanimation.cpp @@ -55,6 +55,7 @@ void WeaponAnimation::attachArrow(MWWorld::Ptr actor) return; std::string model = ammo->getClass().getModel(*ammo); + assert(weapon->mSkelBase && "Need a skeleton to attach the arrow to"); mAmmunition = NifOgre::Loader::createObjects(weapon->mSkelBase, "ArrowBone", weapon->mSkelBase->getParentSceneNode(), model); configureAddedObject(mAmmunition, *ammo, MWWorld::InventoryStore::Slot_Ammunition); } diff --git a/components/nifogre/skeleton.cpp b/components/nifogre/skeleton.cpp index 4f6921d89d..a3fade5b2c 100644 --- a/components/nifogre/skeleton.cpp +++ b/components/nifogre/skeleton.cpp @@ -111,7 +111,7 @@ bool NIFSkeletonLoader::needSkeleton(const Nif::Node *node) /* We need to be a little aggressive here, since some NIFs have a crap-ton * of nodes and Ogre only supports 256 bones. We will skip a skeleton if: * There are no bones used for skinning, there are no keyframe controllers, there - * are no nodes named "AttachLight", and the tree consists of NiNode, + * are no nodes named "AttachLight" or "ArrowBone", and the tree consists of NiNode, * NiTriShape, and RootCollisionNode types only. */ if(node->boneTrafo) @@ -126,7 +126,7 @@ bool NIFSkeletonLoader::needSkeleton(const Nif::Node *node) } while(!(ctrl=ctrl->next).empty()); } - if (node->name == "AttachLight") + if (node->name == "AttachLight" || node->name == "ArrowBone") return true; if(node->recType == Nif::RC_NiNode || node->recType == Nif::RC_RootCollisionNode) From 5a16420231c42d2a15feead1173aea899c0c4e76 Mon Sep 17 00:00:00 2001 From: Sergey Shnatsel Davidoff Date: Sun, 23 Nov 2014 19:47:53 +0300 Subject: [PATCH 112/303] supply the correct icon (without the red line) for the default rendering state --- files/opencs/scene-view-status-31.png | Bin 3553 -> 3441 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/files/opencs/scene-view-status-31.png b/files/opencs/scene-view-status-31.png index 3f8be3ac17fd5ed55683a52356924b4d2415920e..16d80af04eaf6b0fa840ce5b24ff381e60101368 100644 GIT binary patch delta 3255 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 delta 3368 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 From ada4e375564d72d2bf739ee0b46622fe590fbff6 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 24 Nov 2014 01:02:18 +0100 Subject: [PATCH 113/303] Fix race preview texture not being destroyed properly (Fixes #2098) --- apps/openmw/mwgui/race.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/openmw/mwgui/race.cpp b/apps/openmw/mwgui/race.cpp index 2bfc4f1416..444b572b68 100644 --- a/apps/openmw/mwgui/race.cpp +++ b/apps/openmw/mwgui/race.cpp @@ -162,6 +162,9 @@ namespace MWGui void RaceDialog::close() { + mPreviewImage->setImageTexture(""); + const std::string textureName = "CharacterHeadPreview"; + MyGUI::RenderManager::getInstance().destroyTexture(MyGUI::RenderManager::getInstance().getTexture(textureName)); mPreview.reset(NULL); } From 69134e3ac27e6986c16a8cca68b99dcf6019a9a2 Mon Sep 17 00:00:00 2001 From: cc9cii Date: Mon, 24 Nov 2014 12:48:36 +1100 Subject: [PATCH 114/303] Add icon to windows executable. --- apps/wizard/CMakeLists.txt | 4 ++++ files/windows/openmw-wizard.ico | Bin 0 -> 370070 bytes files/windows/openmw-wizard.rc | 1 + 3 files changed, 5 insertions(+) create mode 100644 files/windows/openmw-wizard.ico create mode 100644 files/windows/openmw-wizard.rc diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index 61d6c5123f..b8cc3fda4d 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -25,6 +25,10 @@ set(WIZARD utils/componentlistwidget.cpp ) +if(WIN32) + list(APPEND WIZARD ${CMAKE_SOURCE_DIR}/files/windows/openmw-wizard.rc) +endif() + set(WIZARD_HEADER componentselectionpage.hpp conclusionpage.hpp diff --git a/files/windows/openmw-wizard.ico b/files/windows/openmw-wizard.ico new file mode 100644 index 0000000000000000000000000000000000000000..e5dd50ba631ce5e65e90c7969ba2872356829880 GIT binary patch literal 370070 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�R#2v$r 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 Date: Tue, 25 Nov 2014 02:41:54 +0100 Subject: [PATCH 115/303] Apply the frame time cap to Ogre's ControllerManager (Fixes #2154) This fixes particle systems getting out of whack due to a particularly long frame time, e.g. after a loading screen. --- apps/openmw/engine.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 2eebb8c28f..d7b23c0966 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -79,8 +79,7 @@ bool OMW::Engine::frameRenderingQueued (const Ogre::FrameEvent& evt) { try { - float frametime = std::min(evt.timeSinceLastFrame, 0.2f); - + float frametime = evt.timeSinceLastFrame; mEnvironment.setFrameDuration (frametime); // update input @@ -478,9 +477,15 @@ void OMW::Engine::go() } // Start the main rendering loop + Ogre::Timer timer; while (!MWBase::Environment::get().getStateManager()->hasQuitRequest()) - Ogre::Root::getSingleton().renderOneFrame(); + { + float dt = timer.getMilliseconds()/1000.f; + dt = std::min(dt, 0.2f); + timer.reset(); + Ogre::Root::getSingleton().renderOneFrame(dt); + } // Save user settings settings.saveUser(settingspath); From 238325455d6a15ae12e162ee796fae5fe8cab809 Mon Sep 17 00:00:00 2001 From: MiroslavR Date: Sun, 23 Nov 2014 12:40:55 +0100 Subject: [PATCH 116/303] Erase effects that have expired (Fixes #2134) --- apps/openmw/mwmechanics/activespells.cpp | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index e64a736c3f..d87e225434 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -17,18 +17,35 @@ namespace MWMechanics MWWorld::TimeStamp now = MWBase::Environment::get().getWorld()->getTimeStamp(); - // Erase no longer active spells + // Erase no longer active spells and effects if (mLastUpdate!=now) { TContainer::iterator iter (mSpells.begin()); while (iter!=mSpells.end()) + { if (!timeToExpire (iter)) { mSpells.erase (iter++); rebuild = true; } else + { + std::vector& effects = iter->second.mEffects; + for (std::vector::iterator effectIt = effects.begin(); effectIt != effects.end();) + { + MWWorld::TimeStamp start = iter->second.mTimeStamp; + MWWorld::TimeStamp end = start + static_cast(effectIt->mDuration)*MWBase::Environment::get().getWorld()->getTimeScaleFactor()/(60*60); + if (end <= now) + { + effectIt = effects.erase(effectIt); + rebuild = true; + } + else + ++effectIt; + } ++iter; + } + } mLastUpdate = now; } From ff8bdd74ed32e2b2151156cd9149648d94bb094e Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 25 Nov 2014 15:54:55 +0100 Subject: [PATCH 117/303] Fix strange bitflags handling --- apps/wizard/componentselectionpage.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index d372f677d0..956f0f2379 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -62,7 +62,7 @@ void Wizard::ComponentSelectionPage::initializePage() if (field(QLatin1String("installation.new")).toBool() == true) { - morrowindItem->setFlags(morrowindItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); + morrowindItem->setFlags(morrowindItem->flags() & ~Qt::ItemIsEnabled | Qt::ItemIsUserCheckable); morrowindItem->setData(Qt::CheckStateRole, Qt::Checked); componentsList->addItem(morrowindItem); @@ -77,7 +77,7 @@ void Wizard::ComponentSelectionPage::initializePage() if (mWizard->mInstallations[path].hasMorrowind) { morrowindItem->setText(tr("Morrowind\t\t(installed)")); - morrowindItem->setFlags(morrowindItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); + morrowindItem->setFlags(morrowindItem->flags() & ~Qt::ItemIsEnabled | Qt::ItemIsUserCheckable); morrowindItem->setData(Qt::CheckStateRole, Qt::Unchecked); } else { morrowindItem->setText(tr("Morrowind")); @@ -88,7 +88,7 @@ void Wizard::ComponentSelectionPage::initializePage() if (mWizard->mInstallations[path].hasTribunal) { tribunalItem->setText(tr("Tribunal\t\t(installed)")); - tribunalItem->setFlags(tribunalItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); + tribunalItem->setFlags(tribunalItem->flags() & ~Qt::ItemIsEnabled | Qt::ItemIsUserCheckable); tribunalItem->setData(Qt::CheckStateRole, Qt::Unchecked); } else { tribunalItem->setText(tr("Tribunal")); @@ -99,7 +99,7 @@ void Wizard::ComponentSelectionPage::initializePage() if (mWizard->mInstallations[path].hasBloodmoon) { bloodmoonItem->setText(tr("Bloodmoon\t\t(installed)")); - bloodmoonItem->setFlags(bloodmoonItem->flags() & !Qt::ItemIsEnabled & Qt::ItemIsUserCheckable); + bloodmoonItem->setFlags(bloodmoonItem->flags() & ~Qt::ItemIsEnabled | Qt::ItemIsUserCheckable); bloodmoonItem->setData(Qt::CheckStateRole, Qt::Unchecked); } else { bloodmoonItem->setText(tr("Bloodmoon")); From 936094ae950897fe43f64809b3ed025bc7987d2a Mon Sep 17 00:00:00 2001 From: cc9cii Date: Wed, 26 Nov 2014 08:08:28 +1100 Subject: [PATCH 118/303] Set range of spinbox types in dialogsubview. --- apps/opencs/view/world/util.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/opencs/view/world/util.cpp b/apps/opencs/view/world/util.cpp index f8847abfcd..f987c8d9f2 100644 --- a/apps/opencs/view/world/util.cpp +++ b/apps/opencs/view/world/util.cpp @@ -2,6 +2,8 @@ #include "util.hpp" #include +#include +#include #include #include @@ -157,16 +159,22 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO return new QLineEdit(parent); case CSMWorld::ColumnBase::Display_Integer: - - return new QSpinBox(parent); + { + QSpinBox *sb = new QSpinBox(parent); + sb->setRange(INT_MIN, INT_MAX); + return sb; + } case CSMWorld::ColumnBase::Display_Var: return new QLineEdit(parent); case CSMWorld::ColumnBase::Display_Float: - - return new QDoubleSpinBox(parent); + { + QDoubleSpinBox *dsb = new QDoubleSpinBox(parent); + dsb->setRange(FLT_MIN, FLT_MAX); + return dsb; + } case CSMWorld::ColumnBase::Display_LongString: { From 54ac54b347ae9edc32f70336c72303d1c03f0869 Mon Sep 17 00:00:00 2001 From: Sergey Shnatsel Davidoff Date: Wed, 26 Nov 2014 00:20:45 +0300 Subject: [PATCH 119/303] Add the "scene-play.png" icon for launching the game from OpenCS. Also included are SVG source and the GIMP XCF with transparency mask on the edges. --- .../opencs/raster/scene-play-rev9_masked.xcf | Bin 0 -> 8365 bytes files/opencs/scalable/scene-play-rev9.svg | 972 ++++++++++++++++++ files/opencs/scene-play.png | Bin 0 -> 3601 bytes 3 files changed, 972 insertions(+) create mode 100644 files/opencs/raster/scene-play-rev9_masked.xcf create mode 100644 files/opencs/scalable/scene-play-rev9.svg create mode 100644 files/opencs/scene-play.png diff --git a/files/opencs/raster/scene-play-rev9_masked.xcf b/files/opencs/raster/scene-play-rev9_masked.xcf new file mode 100644 index 0000000000000000000000000000000000000000..4f7a06d065d343b98b179e3141b71b43800695db GIT binary patch literal 8365 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 literal 0 HcmV?d00001 diff --git a/files/opencs/scalable/scene-play-rev9.svg b/files/opencs/scalable/scene-play-rev9.svg new file mode 100644 index 0000000000..59a479310e --- /dev/null +++ b/files/opencs/scalable/scene-play-rev9.svg @@ -0,0 +1,972 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/opencs/scene-play.png b/files/opencs/scene-play.png new file mode 100644 index 0000000000000000000000000000000000000000..e2fabea7d9f80d5820085428ba5b82a0fc0e8b8a GIT binary patch literal 3601 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 XE>yyZSdFAp00000NkvXXu0mjfgA3Z> literal 0 HcmV?d00001 From f1ff2b2d6a4456da80edeb0b58f881dd46410dc7 Mon Sep 17 00:00:00 2001 From: Sergey Shnatsel Davidoff Date: Wed, 26 Nov 2014 00:28:11 +0300 Subject: [PATCH 120/303] Add offscreen credits and a note about the use of "Play" icon from elementary icon theme to the SVG masters. Clean up unused offscreen objects in scene view icon. --- files/opencs/scalable/scene-play-rev9.svg | 48 ++++++++++++--- .../scalable/scene-view-status-rev14.svg | 59 ++++++++++--------- 2 files changed, 71 insertions(+), 36 deletions(-) diff --git a/files/opencs/scalable/scene-play-rev9.svg b/files/opencs/scalable/scene-play-rev9.svg index 59a479310e..26e7de1e8f 100644 --- a/files/opencs/scalable/scene-play-rev9.svg +++ b/files/opencs/scalable/scene-play-rev9.svg @@ -15,7 +15,7 @@ id="svg2" version="1.1" inkscape:version="0.48.3.1 r9886" - sodipodi:docname="14_play9_.svg"> + sodipodi:docname="scene-play-rev9.svg"> + + + Sergey "Shnatsel" Davidoff <shnatsel@gmail.com> + + + + + Sergey "Shnatsel" Davidoff <shnatsel@gmail.com> + + + @@ -947,8 +959,7 @@ + inkscape:label="play"> @@ -968,5 +979,28 @@ inkscape:connector-curvature="0" d="m 4.5035809,44.49642 0,-39.9928394 L 43.49642,24.500001 4.5035809,44.49642 z" /> + the "play" sign is taken fromelementary icon themeby Daniel Forelicensed under GNU GPLv3 diff --git a/files/opencs/scalable/scene-view-status-rev14.svg b/files/opencs/scalable/scene-view-status-rev14.svg index bb2eab25db..0724babe78 100644 --- a/files/opencs/scalable/scene-view-status-rev14.svg +++ b/files/opencs/scalable/scene-view-status-rev14.svg @@ -273,12 +273,12 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="16" - inkscape:cx="2.950497" - inkscape:cy="24.209766" + inkscape:zoom="4" + inkscape:cx="19.041611" + inkscape:cy="-5.7168931" inkscape:document-units="px" inkscape:current-layer="layer3" - showgrid="true" + showgrid="false" inkscape:snap-page="true" showguides="true" inkscape:guide-bbox="true" @@ -309,7 +309,7 @@ image/svg+xml - + Sergey "Shnatsel" Davidoff <shnatsel@gmail.com> @@ -332,28 +332,6 @@ transform="translate(0,-1004.3622)" style="display:inline" sodipodi:insensitive="true"> - - - - + This icon was designed bySergey "Shnatsel" Davidofffor OpenMW content editorLicensed under GNU GPLv3 + style="display:inline" + sodipodi:insensitive="true"> Date: Thu, 27 Nov 2014 08:59:21 +0100 Subject: [PATCH 121/303] fixed missing tooltip update for toggle/mode-type buttons --- apps/opencs/view/widget/pushbutton.cpp | 10 ++++++++++ apps/opencs/view/widget/pushbutton.hpp | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/apps/opencs/view/widget/pushbutton.cpp b/apps/opencs/view/widget/pushbutton.cpp index f234625859..d4e6007944 100644 --- a/apps/opencs/view/widget/pushbutton.cpp +++ b/apps/opencs/view/widget/pushbutton.cpp @@ -72,6 +72,11 @@ CSVWidget::PushButton::PushButton (const QIcon& icon, Type type, const QString& 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); setExtendedToolTip(); } @@ -96,4 +101,9 @@ QString CSVWidget::PushButton::getBaseToolTip() const CSVWidget::PushButton::Type CSVWidget::PushButton::getType() const { return mType; +} + +void CSVWidget::PushButton::checkedStateChanged (bool checked) +{ + setExtendedToolTip(); } \ No newline at end of file diff --git a/apps/opencs/view/widget/pushbutton.hpp b/apps/opencs/view/widget/pushbutton.hpp index 35062a137b..09cf22757d 100644 --- a/apps/opencs/view/widget/pushbutton.hpp +++ b/apps/opencs/view/widget/pushbutton.hpp @@ -53,6 +53,10 @@ namespace CSVWidget QString getBaseToolTip() const; Type getType() const; + + private slots: + + void checkedStateChanged (bool checked); }; } From 50a489321fbc5c4bbc91652de9cd9ffc5306a813 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 27 Nov 2014 09:27:29 +0100 Subject: [PATCH 122/303] updated run button --- apps/opencs/view/render/worldspacewidget.cpp | 2 +- apps/opencs/view/widget/scenetoolrun.cpp | 8 ++++---- apps/opencs/view/widget/scenetoolrun.hpp | 4 +--- files/opencs/resources.qrc | 1 + 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 6c6acd22dc..a5d1ba5465 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -172,7 +172,7 @@ CSVWidget::SceneToolRun *CSVRender::WorldspaceWidget::makeRunTool ( std::sort (profiles.begin(), profiles.end()); mRun = new CSVWidget::SceneToolRun (parent, "Run OpenMW from the current camera position", - ":placeholder", ":placeholder", profiles); + ":scenetoolbar/play", profiles); connect (mRun, SIGNAL (runRequest (const std::string&)), this, SLOT (runRequest (const std::string&))); diff --git a/apps/opencs/view/widget/scenetoolrun.cpp b/apps/opencs/view/widget/scenetoolrun.cpp index 92f3193feb..0c7a4b9f0d 100644 --- a/apps/opencs/view/widget/scenetoolrun.cpp +++ b/apps/opencs/view/widget/scenetoolrun.cpp @@ -26,7 +26,7 @@ void CSVWidget::SceneToolRun::adjustToolTips() void CSVWidget::SceneToolRun::updateIcon() { - setIcon (QIcon (mSelected==mProfiles.end() ? mIconDisabled : mIcon)); + setDisabled (mSelected==mProfiles.end()); } void CSVWidget::SceneToolRun::updatePanel() @@ -46,11 +46,11 @@ void CSVWidget::SceneToolRun::updatePanel() } CSVWidget::SceneToolRun::SceneToolRun (SceneToolbar *parent, const QString& toolTip, - const QString& icon, const QString& iconDisabled, const std::vector& profiles) + const QString& icon, const std::vector& profiles) : SceneTool (parent, Type_TopAction), mProfiles (profiles.begin(), profiles.end()), - mSelected (mProfiles.begin()), mToolTip (toolTip), mIcon (icon), - mIconDisabled (iconDisabled) + mSelected (mProfiles.begin()), mToolTip (toolTip) { + setIcon (QIcon (icon)); updateIcon(); adjustToolTips(); diff --git a/apps/opencs/view/widget/scenetoolrun.hpp b/apps/opencs/view/widget/scenetoolrun.hpp index 4396c22881..dd035462fe 100644 --- a/apps/opencs/view/widget/scenetoolrun.hpp +++ b/apps/opencs/view/widget/scenetoolrun.hpp @@ -19,8 +19,6 @@ namespace CSVWidget std::set mProfiles; std::set::iterator mSelected; QString mToolTip; - QString mIcon; - QString mIconDisabled; QFrame *mPanel; QTableWidget *mTable; @@ -35,7 +33,7 @@ namespace CSVWidget public: SceneToolRun (SceneToolbar *parent, const QString& toolTip, const QString& icon, - const QString& iconDisabled, const std::vector& profiles); + const std::vector& profiles); virtual void showPanel (const QPoint& position); diff --git a/files/opencs/resources.qrc b/files/opencs/resources.qrc index 751ae64844..86631f4373 100644 --- a/files/opencs/resources.qrc +++ b/files/opencs/resources.qrc @@ -79,5 +79,6 @@ eyeballdude.png flying eye.png orbit2.png + scene-play.png From cb74c1c36e24debbd697f16fd7fd3d2d619bbdbf Mon Sep 17 00:00:00 2001 From: MiroslavR Date: Thu, 27 Nov 2014 20:44:41 +0100 Subject: [PATCH 123/303] Set health to 0 if it drops below 1 (Fixes #2163) --- apps/openmw/mwmechanics/creaturestats.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index c4d316ad62..a287a7e117 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -184,6 +184,7 @@ namespace MWMechanics if (index==0 && mDynamic[index].getCurrent()<1) { mDead = true; + mDynamic[index].setCurrent(0); if (MWBase::Environment::get().getWorld()->getGodModeState()) MWBase::Environment::get().getMechanicsManager()->keepPlayerAlive(); From e04ead2bd53c42a081a8978fb5b3eb3652b46776 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Fri, 28 Nov 2014 09:14:02 +0100 Subject: [PATCH 124/303] new element visibility button icons --- apps/opencs/CMakeLists.txt | 1 + apps/opencs/view/render/elements.hpp | 6 +- .../view/render/pagedworldspacewidget.hpp | 6 +- .../view/render/unpagedworldspacewidget.cpp | 5 +- .../view/render/unpagedworldspacewidget.hpp | 2 +- apps/opencs/view/render/worldspacewidget.cpp | 18 +-- apps/opencs/view/render/worldspacewidget.hpp | 8 +- apps/opencs/view/widget/scenetooltoggle2.cpp | 139 ++++++++++++++++++ apps/opencs/view/widget/scenetooltoggle2.hpp | 76 ++++++++++ apps/opencs/view/world/scenesubview.cpp | 3 +- files/opencs/resources.qrc | 37 +++++ 11 files changed, 280 insertions(+), 21 deletions(-) create mode 100644 apps/opencs/view/widget/scenetooltoggle2.cpp create mode 100644 apps/opencs/view/widget/scenetooltoggle2.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 969058dd7b..9d8dd89e1a 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -73,6 +73,7 @@ opencs_units_noqt (view/world opencs_units (view/widget scenetoolbar scenetool scenetoolmode pushbutton scenetooltoggle scenetoolrun modebutton + scenetooltoggle2 ) opencs_units (view/render diff --git a/apps/opencs/view/render/elements.hpp b/apps/opencs/view/render/elements.hpp index 784e412125..5a37956e9e 100644 --- a/apps/opencs/view/render/elements.hpp +++ b/apps/opencs/view/render/elements.hpp @@ -8,10 +8,10 @@ namespace CSVRender { // elements that are part of the actual scene Element_Reference = 0x1, - Element_Terrain = 0x2, + Element_Pathgrid = 0x2, Element_Water = 0x4, - Element_Pathgrid = 0x8, - Element_Fog = 0x10, + Element_Fog = 0x8, + Element_Terrain = 0x10, // control elements Element_CellMarker = 0x10000, diff --git a/apps/opencs/view/render/pagedworldspacewidget.hpp b/apps/opencs/view/render/pagedworldspacewidget.hpp index 59540a71e6..ca618d1220 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.hpp +++ b/apps/opencs/view/render/pagedworldspacewidget.hpp @@ -8,9 +8,13 @@ #include "worldspacewidget.hpp" #include "cell.hpp" +namespace CSVWidget +{ + class SceneToolToggle; +} + namespace CSVRender { - class TextOverlay; class OverlayMask; diff --git a/apps/opencs/view/render/unpagedworldspacewidget.cpp b/apps/opencs/view/render/unpagedworldspacewidget.cpp index 8012b1b246..1d6dd82c51 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.cpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.cpp @@ -15,6 +15,7 @@ #include "../../model/world/tablemimedata.hpp" #include "../widget/scenetooltoggle.hpp" +#include "../widget/scenetooltoggle2.hpp" #include "elements.hpp" @@ -33,11 +34,11 @@ void CSVRender::UnpagedWorldspaceWidget::update() } void CSVRender::UnpagedWorldspaceWidget::addVisibilitySelectorButtons ( - CSVWidget::SceneToolToggle *tool) + CSVWidget::SceneToolToggle2 *tool) { WorldspaceWidget::addVisibilitySelectorButtons (tool); - tool->addButton (":armor.png", Element_Fog, ":armor.png", "Fog"); + tool->addButton (Element_Fog, "Fog"); } CSVRender::UnpagedWorldspaceWidget::UnpagedWorldspaceWidget (const std::string& cellId, CSMDoc::Document& document, QWidget* parent) diff --git a/apps/opencs/view/render/unpagedworldspacewidget.hpp b/apps/opencs/view/render/unpagedworldspacewidget.hpp index 5924abaa9e..1463e53247 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.hpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.hpp @@ -34,7 +34,7 @@ namespace CSVRender protected: - virtual void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle *tool); + virtual void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle2 *tool); public: diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index a5d1ba5465..993c77688e 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -13,7 +13,7 @@ #include "../../model/world/idtable.hpp" #include "../widget/scenetoolmode.hpp" -#include "../widget/scenetooltoggle.hpp" +#include "../widget/scenetooltoggle2.hpp" #include "../widget/scenetoolrun.hpp" #include "../world/physicsmanager.hpp" @@ -127,10 +127,10 @@ CSVWidget::SceneToolMode *CSVRender::WorldspaceWidget::makeNavigationSelector ( return tool; } -CSVWidget::SceneToolToggle *CSVRender::WorldspaceWidget::makeSceneVisibilitySelector (CSVWidget::SceneToolbar *parent) +CSVWidget::SceneToolToggle2 *CSVRender::WorldspaceWidget::makeSceneVisibilitySelector (CSVWidget::SceneToolbar *parent) { - mSceneElements= new CSVWidget::SceneToolToggle (parent, - "Scene Element Visibility", ":placeholder"); + mSceneElements = new CSVWidget::SceneToolToggle2 (parent, + "Scene Element Visibility", ":scenetoolbar/scene-view-c", ":scenetoolbar/scene-view-"); addVisibilitySelectorButtons (mSceneElements); @@ -260,12 +260,12 @@ unsigned int CSVRender::WorldspaceWidget::getInteractionMask() const } void CSVRender::WorldspaceWidget::addVisibilitySelectorButtons ( - CSVWidget::SceneToolToggle *tool) + CSVWidget::SceneToolToggle2 *tool) { - tool->addButton (":placeholder", Element_Reference, ":placeholder", "References"); - tool->addButton (":placeholder", Element_Terrain, ":placeholder", "Terrain"); - tool->addButton (":placeholder", Element_Water, ":placeholder", "Water"); - tool->addButton (":placeholder", Element_Pathgrid, ":placeholder", "Pathgrid"); + tool->addButton (Element_Reference, "References"); + tool->addButton (Element_Terrain, "Terrain"); + tool->addButton (Element_Water, "Water"); + tool->addButton (Element_Pathgrid, "Pathgrid"); } void CSVRender::WorldspaceWidget::addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool) diff --git a/apps/opencs/view/render/worldspacewidget.hpp b/apps/opencs/view/render/worldspacewidget.hpp index e54eb91e14..550b5d4a9a 100644 --- a/apps/opencs/view/render/worldspacewidget.hpp +++ b/apps/opencs/view/render/worldspacewidget.hpp @@ -18,7 +18,7 @@ namespace CSMWorld namespace CSVWidget { class SceneToolMode; - class SceneToolToggle; + class SceneToolToggle2; class SceneToolbar; class SceneToolRun; } @@ -37,7 +37,7 @@ namespace CSVRender CSVRender::Navigation1st m1st; CSVRender::NavigationFree mFree; CSVRender::NavigationOrbit mOrbit; - CSVWidget::SceneToolToggle *mSceneElements; + CSVWidget::SceneToolToggle2 *mSceneElements; CSVWidget::SceneToolRun *mRun; CSMDoc::Document& mDocument; CSVWorld::PhysicsSystem *mPhysics; @@ -71,7 +71,7 @@ namespace CSVRender /// \attention The created tool is not added to the toolbar (via addTool). Doing /// that is the responsibility of the calling function. - CSVWidget::SceneToolToggle *makeSceneVisibilitySelector ( + CSVWidget::SceneToolToggle2 *makeSceneVisibilitySelector ( CSVWidget::SceneToolbar *parent); /// \attention The created tool is not added to the toolbar (via addTool). Doing @@ -107,7 +107,7 @@ namespace CSVRender protected: - virtual void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle *tool); + virtual void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle2 *tool); virtual void addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool); diff --git a/apps/opencs/view/widget/scenetooltoggle2.cpp b/apps/opencs/view/widget/scenetooltoggle2.cpp new file mode 100644 index 0000000000..1c5c11a4da --- /dev/null +++ b/apps/opencs/view/widget/scenetooltoggle2.cpp @@ -0,0 +1,139 @@ + +#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() +{ + std::ostringstream stream; + stream << mCompositeIcon << getSelection(); + 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 (0) +{ + 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, + const QString& name, const QString& tooltip) +{ + 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); + + mLayout->addWidget (button); + + ButtonDesc desc; + desc.mId = id; + desc.mName = name; + desc.mIndex = 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::SceneToolToggle2::getSelection() const +{ + unsigned int selection = 0; + + for (std::map::const_iterator iter (mButtons.begin()); + iter!=mButtons.end(); ++iter) + if (iter->first->isChecked()) + selection |= iter->second.mId; + + return selection; +} + +void CSVWidget::SceneToolToggle2::setSelection (unsigned int selection) +{ + for (std::map::iterator iter (mButtons.begin()); + iter!=mButtons.end(); ++iter) + iter->first->setChecked (selection & iter->second.mId); + + 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(); + } +} diff --git a/apps/opencs/view/widget/scenetooltoggle2.hpp b/apps/opencs/view/widget/scenetooltoggle2.hpp new file mode 100644 index 0000000000..4bd9ba26f6 --- /dev/null +++ b/apps/opencs/view/widget/scenetooltoggle2.hpp @@ -0,0 +1,76 @@ +#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 mId; + 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); + + virtual void showPanel (const QPoint& position); + + /// \attention After the last button has been added, setSelection must be called at + /// least once to finalise the layout. + void addButton (unsigned int id, + const QString& name, const QString& tooltip = ""); + + unsigned int getSelection() const; + + /// \param or'ed button IDs. IDs that do not exist will be ignored. + void setSelection (unsigned int selection); + + signals: + + void selectionChanged(); + + private slots: + + void selected(); + }; +} + +#endif diff --git a/apps/opencs/view/world/scenesubview.cpp b/apps/opencs/view/world/scenesubview.cpp index 5ddefcc6b2..3fdf2f6e5c 100644 --- a/apps/opencs/view/world/scenesubview.cpp +++ b/apps/opencs/view/world/scenesubview.cpp @@ -20,6 +20,7 @@ #include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolmode.hpp" #include "../widget/scenetooltoggle.hpp" +#include "../widget/scenetooltoggle2.hpp" #include "../widget/scenetoolrun.hpp" #include "tablebottombox.hpp" @@ -109,7 +110,7 @@ CSVWidget::SceneToolbar* CSVWorld::SceneSubView::makeToolbar (CSVRender::Worldsp CSVWidget::SceneToolMode *lightingTool = widget->makeLightingSelector (toolbar); toolbar->addTool (lightingTool); - CSVWidget::SceneToolToggle *sceneVisibilityTool = + CSVWidget::SceneToolToggle2 *sceneVisibilityTool = widget->makeSceneVisibilitySelector (toolbar); toolbar->addTool (sceneVisibilityTool); diff --git a/files/opencs/resources.qrc b/files/opencs/resources.qrc index 86631f4373..0cc6996a8f 100644 --- a/files/opencs/resources.qrc +++ b/files/opencs/resources.qrc @@ -80,5 +80,42 @@ flying eye.png orbit2.png scene-play.png + scene-view-references.png + scene-view-terrain.png + scene-view-water.png + scene-view-pathgrid.png + scene-view-fog.png + scene-view-status-0.png + scene-view-status-1.png + scene-view-status-2.png + scene-view-status-3.png + scene-view-status-4.png + scene-view-status-5.png + scene-view-status-6.png + scene-view-status-7.png + scene-view-status-8.png + scene-view-status-9.png + scene-view-status-10.png + scene-view-status-11.png + scene-view-status-12.png + scene-view-status-13.png + scene-view-status-14.png + scene-view-status-15.png + scene-view-status-16.png + scene-view-status-17.png + scene-view-status-18.png + scene-view-status-19.png + scene-view-status-20.png + scene-view-status-21.png + scene-view-status-22.png + scene-view-status-23.png + scene-view-status-24.png + scene-view-status-25.png + scene-view-status-26.png + scene-view-status-27.png + scene-view-status-28.png + scene-view-status-29.png + scene-view-status-30.png + scene-view-status-31.png From e177b66c1d5cfec147bc892171f5ac0feec3f56f Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Fri, 28 Nov 2014 09:16:39 +0100 Subject: [PATCH 125/303] moved fog button from unpaged worldspace to worldspace --- apps/opencs/view/render/unpagedworldspacewidget.cpp | 8 -------- apps/opencs/view/render/unpagedworldspacewidget.hpp | 4 ---- apps/opencs/view/render/worldspacewidget.cpp | 1 + 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/apps/opencs/view/render/unpagedworldspacewidget.cpp b/apps/opencs/view/render/unpagedworldspacewidget.cpp index 1d6dd82c51..07acbe493c 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.cpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.cpp @@ -33,14 +33,6 @@ void CSVRender::UnpagedWorldspaceWidget::update() flagAsModified(); } -void CSVRender::UnpagedWorldspaceWidget::addVisibilitySelectorButtons ( - CSVWidget::SceneToolToggle2 *tool) -{ - WorldspaceWidget::addVisibilitySelectorButtons (tool); - - tool->addButton (Element_Fog, "Fog"); -} - CSVRender::UnpagedWorldspaceWidget::UnpagedWorldspaceWidget (const std::string& cellId, CSMDoc::Document& document, QWidget* parent) : WorldspaceWidget (document, parent), mCellId (cellId) { diff --git a/apps/opencs/view/render/unpagedworldspacewidget.hpp b/apps/opencs/view/render/unpagedworldspacewidget.hpp index 1463e53247..237cb8f46f 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.hpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.hpp @@ -32,10 +32,6 @@ namespace CSVRender void update(); - protected: - - virtual void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle2 *tool); - public: UnpagedWorldspaceWidget (const std::string& cellId, CSMDoc::Document& document, diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 993c77688e..1f7c415122 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -266,6 +266,7 @@ void CSVRender::WorldspaceWidget::addVisibilitySelectorButtons ( tool->addButton (Element_Terrain, "Terrain"); tool->addButton (Element_Water, "Water"); tool->addButton (Element_Pathgrid, "Pathgrid"); + tool->addButton (Element_Fog, "Fog"); } void CSVRender::WorldspaceWidget::addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool) From 0a466ad643887b8fe1fdd0770736652a1dae2904 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 28 Nov 2014 14:45:35 +0100 Subject: [PATCH 126/303] Make recalculation of magicka less aggressive (Fixes #2155) --- apps/openmw/mwmechanics/actors.cpp | 9 ------ apps/openmw/mwmechanics/creaturestats.cpp | 34 +++++++++++++++-------- apps/openmw/mwmechanics/creaturestats.hpp | 3 +- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 2e835d57e4..a3cfbfd49a 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -396,11 +396,7 @@ namespace MWMechanics { CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr); - int strength = creatureStats.getAttribute(ESM::Attribute::Strength).getModified(); int intelligence = creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified(); - int willpower = creatureStats.getAttribute(ESM::Attribute::Willpower).getModified(); - int agility = creatureStats.getAttribute(ESM::Attribute::Agility).getModified(); - int endurance = creatureStats.getAttribute(ESM::Attribute::Endurance).getModified(); float base = 1.f; if (ptr.getCellRef().getRefId() == "player") @@ -415,11 +411,6 @@ namespace MWMechanics float diff = (static_cast(magickaFactor*intelligence)) - magicka.getBase(); magicka.modify(diff); creatureStats.setMagicka(magicka); - - DynamicStat fatigue = creatureStats.getFatigue(); - diff = (strength+willpower+agility+endurance) - fatigue.getBase(); - fatigue.modify(diff); - creatureStats.setFatigue(fatigue); } void Actors::restoreDynamicStats (const MWWorld::Ptr& ptr, bool sleep) diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index c4d316ad62..6d342833e6 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -20,7 +20,7 @@ namespace MWMechanics mAttacked (false), mAttackingOrSpell(false), mIsWerewolf(false), - mFallHeight(0), mRecalcDynamicStats(false), mKnockdown(false), mKnockdownOneFrame(false), + mFallHeight(0), mRecalcMagicka(false), mKnockdown(false), mKnockdownOneFrame(false), mKnockdownOverOneFrame(false), mHitRecovery(false), mBlock(false), mMovementFlags(0), mDrawState (DrawState_Nothing), mAttackStrength(0.f), mLastRestock(0,0), mGoldPool(0), mActorId(-1), @@ -147,10 +147,22 @@ namespace MWMechanics if (value != currentValue) { - if (index != ESM::Attribute::Luck - && index != ESM::Attribute::Personality - && index != ESM::Attribute::Speed) - mRecalcDynamicStats = true; + 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) + { + int strength = getAttribute(ESM::Attribute::Strength).getModified(); + int willpower = getAttribute(ESM::Attribute::Willpower).getModified(); + int agility = getAttribute(ESM::Attribute::Agility).getModified(); + int endurance = getAttribute(ESM::Attribute::Endurance).getModified(); + DynamicStat fatigue = getFatigue(); + float diff = (strength+willpower+agility+endurance) - fatigue.getBase(); + fatigue.modify(diff); + setFatigue(fatigue); + } } if(!mIsWerewolf) @@ -199,7 +211,7 @@ namespace MWMechanics { if (effects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier() != mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier()) - mRecalcDynamicStats = true; + mRecalcMagicka = true; mMagicEffects.setModifiers(effects); } @@ -360,9 +372,9 @@ namespace MWMechanics bool CreatureStats::needToRecalcDynamicStats() { - if (mRecalcDynamicStats) + if (mRecalcMagicka) { - mRecalcDynamicStats = false; + mRecalcMagicka = false; return true; } return false; @@ -370,7 +382,7 @@ namespace MWMechanics void CreatureStats::setNeedRecalcDynamicStats(bool val) { - mRecalcDynamicStats = val; + mRecalcMagicka = val; } void CreatureStats::setKnockedDown(bool value) @@ -497,7 +509,7 @@ namespace MWMechanics state.mAttackStrength = mAttackStrength; state.mFallHeight = mFallHeight; // TODO: vertical velocity (move from PhysicActor to CreatureStats?) state.mLastHitObject = mLastHitObject; - state.mRecalcDynamicStats = mRecalcDynamicStats; + state.mRecalcDynamicStats = mRecalcMagicka; state.mDrawState = mDrawState; state.mLevel = mLevel; state.mActorId = mActorId; @@ -545,7 +557,7 @@ namespace MWMechanics mAttackStrength = state.mAttackStrength; mFallHeight = state.mFallHeight; mLastHitObject = state.mLastHitObject; - mRecalcDynamicStats = state.mRecalcDynamicStats; + mRecalcMagicka = state.mRecalcDynamicStats; mDrawState = DrawState_(state.mDrawState); mLevel = state.mLevel; mActorId = state.mActorId; diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index 5e169ffb03..f830dd310c 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -53,8 +53,7 @@ namespace MWMechanics std::string mLastHitObject; // The last object to hit this actor - // Do we need to recalculate stats derived from attributes or other factors? - bool mRecalcDynamicStats; + bool mRecalcMagicka; // For merchants: the last time items were restocked and gold pool refilled. MWWorld::TimeStamp mLastRestock; From d7220cdc2fd45f196467aed8ee4fbe23741df211 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 28 Nov 2014 14:48:03 +0100 Subject: [PATCH 127/303] Do not allow decrease below zero in modCurrentMagicka and modCurrentHealth (Fixes #2158) --- apps/openmw/mwscript/statsextensions.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp index befb8d82ea..d5647db10d 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -278,7 +278,9 @@ namespace MWScript MWMechanics::DynamicStat stat (ptr.getClass().getCreatureStats (ptr) .getDynamic (mIndex)); - stat.setCurrent (diff + current, true); + // for fatigue, a negative current value is allowed and means the actor will be knocked down + bool allowDecreaseBelowZero = (mIndex == 2); + stat.setCurrent (diff + current, allowDecreaseBelowZero); ptr.getClass().getCreatureStats (ptr).setDynamic (mIndex, stat); } From ea8f617508ceb536225c8a097c6bcc1bf7b7b7f0 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 28 Nov 2014 15:54:38 +0100 Subject: [PATCH 128/303] Add missing player control enabled checks (Fixes #2152) --- apps/openmw/mwinput/inputmanagerimp.cpp | 28 ++++++++++++++++--------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 6cb3a5ec59..68682abeac 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -190,14 +190,12 @@ namespace MWInput int action = channel->getNumber(); - if (action == A_Use) + if (mControlSwitch["playercontrols"]) { - mPlayer->getPlayer().getClass().getCreatureStats(mPlayer->getPlayer()).setAttackingOrSpell(currentValue); - } - - if (action == A_Jump) - { - mAttemptJump = (currentValue == 1.0 && previousValue == 0.0); + if (action == A_Use) + mPlayer->getPlayer().getClass().getCreatureStats(mPlayer->getPlayer()).setAttackingOrSpell(currentValue); + else if (action == A_Jump) + mAttemptJump = (currentValue == 1.0 && previousValue == 0.0); } if (currentValue == 1) @@ -622,7 +620,7 @@ namespace MWInput mPlayer->pitch(y); } - if (arg.zrel && mControlSwitch["playerviewswitch"]) //Check to make sure you are allowed to zoomout and there is a change + if (arg.zrel && mControlSwitch["playerviewswitch"] && mControlSwitch["playercontrols"]) //Check to make sure you are allowed to zoomout and there is a change { MWBase::Environment::get().getWorld()->changeVanityModeScale(arg.zrel); MWBase::Environment::get().getWorld()->setCameraDistance(arg.zrel, true, true); @@ -680,7 +678,7 @@ namespace MWInput if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; // Not allowed before the magic window is accessible - if (!mControlSwitch["playermagic"]) + if (!mControlSwitch["playermagic"] || !mControlSwitch["playercontrols"]) return; // Not allowed if no spell selected @@ -701,7 +699,7 @@ namespace MWInput if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; // Not allowed before the inventory window is accessible - if (!mControlSwitch["playerfighting"]) + if (!mControlSwitch["playerfighting"] || !mControlSwitch["playercontrols"]) return; MWMechanics::DrawState_ state = mPlayer->getDrawState(); @@ -713,6 +711,9 @@ namespace MWInput void InputManager::rest() { + if (!mControlSwitch["playercontrols"]) + return; + if (!MWBase::Environment::get().getWindowManager()->getRestEnabled () || MWBase::Environment::get().getWindowManager()->isGuiMode ()) return; @@ -734,6 +735,9 @@ namespace MWInput void InputManager::toggleInventory() { + if (!mControlSwitch["playercontrols"]) + return; + if (MyGUI::InputManager::getInstance ().isModalAny()) return; @@ -770,6 +774,8 @@ namespace MWInput void InputManager::toggleJournal() { + if (!mControlSwitch["playercontrols"]) + return; if (MyGUI::InputManager::getInstance ().isModalAny()) return; @@ -787,6 +793,8 @@ namespace MWInput void InputManager::quickKey (int index) { + if (!mControlSwitch["playercontrols"]) + return; MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); if (player.getClass().getNpcStats(player).isWerewolf()) { From 4fd3a994e9eb199ce8154ac9fba0f22242824e54 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 28 Nov 2014 16:02:43 +0100 Subject: [PATCH 129/303] Add model and script information to BetaComment --- apps/openmw/mwscript/miscextensions.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index c7d221139c..e8784f8de2 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -964,6 +964,9 @@ namespace MWScript msg << "Grid: " << cell->getCell()->getGridX() << " " << cell->getCell()->getGridY() << std::endl; Ogre::Vector3 pos (ptr.getRefData().getPosition().pos); msg << "Coordinates: " << pos << std::endl; + msg << "Model: " << ptr.getClass().getModel(ptr) << std::endl; + if (!ptr.getClass().getScript(ptr).empty()) + msg << "Script: " << ptr.getClass().getScript(ptr) << std::endl; } std::string notes = runtime.getStringLiteral (runtime[0].mInteger); From 5f5fcc2feff49cce7933b6b5df79d8c856e015ae Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 28 Nov 2014 17:15:31 +0100 Subject: [PATCH 130/303] Make PlayGroup use an indefinite number of loops (Fixes #2156) --- apps/openmw/mwmechanics/character.cpp | 2 ++ apps/openmw/mwrender/animation.cpp | 11 +++++++++++ apps/openmw/mwrender/animation.hpp | 4 ++++ apps/openmw/mwscript/animationextensions.cpp | 2 +- 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 72a9bbfde2..11484ac49a 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1645,6 +1645,8 @@ void CharacterController::playGroup(const std::string &groupname, int mode, int } else if(mode == 0) { + if (!mAnimQueue.empty()) + mAnimation->stopLooping(mAnimQueue.front().first); mAnimQueue.resize(1); mAnimQueue.push_back(std::make_pair(groupname, count-1)); } diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 29db648d03..fc147ce59a 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -842,6 +842,17 @@ void Animation::changeGroups(const std::string &groupname, int groups) return; } } + +void Animation::stopLooping(const std::string& groupname) +{ + AnimStateMap::iterator stateiter = mStates.find(groupname); + if(stateiter != mStates.end()) + { + stateiter->second.mLoopCount = 0; + return; + } +} + void Animation::play(const std::string &groupname, int priority, int groups, bool autodisable, float speedmult, const std::string &start, const std::string &stop, float startpoint, size_t loops) { if(!mSkelBase || mAnimSources.empty()) diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 8ca3582dc1..a8a9ee11e6 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -260,6 +260,10 @@ public: float speedmult, const std::string &start, const std::string &stop, float startpoint, size_t loops); + /** If the given animation group is currently playing, set its remaining loop count to '0'. + */ + void stopLooping(const std::string& groupName); + /** Adjust the speed multiplier of an already playing animation. */ void adjustSpeedMult (const std::string& groupname, float speedmult); diff --git a/apps/openmw/mwscript/animationextensions.cpp b/apps/openmw/mwscript/animationextensions.cpp index 52de8e0421..613cf7d24e 100644 --- a/apps/openmw/mwscript/animationextensions.cpp +++ b/apps/openmw/mwscript/animationextensions.cpp @@ -55,7 +55,7 @@ namespace MWScript throw std::runtime_error ("animation mode out of range"); } - MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, 1); + MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, std::numeric_limits::max()); } }; From ad38345de4aa9cf82e0042fafb93d63582429854 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 28 Nov 2014 22:23:42 +0100 Subject: [PATCH 131/303] Clean up listener in destruction of OgrePlatform (Fixes #2145) --- extern/shiny/Platforms/Ogre/OgrePlatform.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extern/shiny/Platforms/Ogre/OgrePlatform.cpp b/extern/shiny/Platforms/Ogre/OgrePlatform.cpp index eab8f93e28..aa01c8ba14 100644 --- a/extern/shiny/Platforms/Ogre/OgrePlatform.cpp +++ b/extern/shiny/Platforms/Ogre/OgrePlatform.cpp @@ -54,6 +54,8 @@ namespace sh OgrePlatform::~OgrePlatform () { + Ogre::MaterialManager::getSingleton().removeListener(this); + delete sSerializer; } From 33c454e0734d0592eb46ae416c1e775185922dc8 Mon Sep 17 00:00:00 2001 From: cc9cii Date: Sat, 29 Nov 2014 20:39:25 +1100 Subject: [PATCH 132/303] Check whether any subrecords remain after skipping moved references. Should resolve bug #2070. --- components/esm/loadcell.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/components/esm/loadcell.cpp b/components/esm/loadcell.cpp index bbd6696f11..f8966ad207 100644 --- a/components/esm/loadcell.cpp +++ b/components/esm/loadcell.cpp @@ -182,6 +182,13 @@ bool Cell::getNextRef(ESMReader &esm, CellRef &ref, bool& deleted) // That should be it, I haven't seen any other fields yet. } + // If moved references are not handled then it is possible that CellRef::load() can lead + // to strange results because ESMReader::isNextSub("xxx") will always return false when + // there are no more subrecords. When moved references are handled properly by OpenCS + // below 2 lines can be removed. + if (!esm.hasMoreSubs()) + return false; + ref.load (esm); // Identify references belonging to a parent file and adapt the ID accordingly. From 5fa7536427d724c9ffbec43e29e56c44d6b5dad9 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 29 Nov 2014 16:50:42 +0100 Subject: [PATCH 133/303] Fix incorrect box shape translation reset Fixes incorrect placement of collision box for "azura spirit_trib" --- components/nifbullet/bulletnifloader.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index b366216de1..3abe0c1716 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -133,8 +133,6 @@ void ManualBulletShapeLoader::loadResource(Ogre::Resource *resource) mResourceName = mShape->getName(); mShape->mCollide = false; mBoundingBox = NULL; - mShape->mBoxTranslation = Ogre::Vector3(0,0,0); - mShape->mBoxRotation = Ogre::Quaternion::IDENTITY; mStaticMesh = NULL; mCompoundShape = NULL; From d3d5c1fd1546ae3d4731453c920d74d9fda2eabb Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 29 Nov 2014 16:51:45 +0100 Subject: [PATCH 134/303] Disable debug drawing for raycasting shapes This reduces performance too much, and seeing both shapes overlaid on top of each other is confusing anyway. This can be reintroduced via a setting if necessary. --- libs/openengine/bullet/physic.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/openengine/bullet/physic.cpp b/libs/openengine/bullet/physic.cpp index d7db81595b..0b08e28d95 100644 --- a/libs/openengine/bullet/physic.cpp +++ b/libs/openengine/bullet/physic.cpp @@ -514,6 +514,7 @@ namespace Physic assert (mRaycastingObjectMap.find(name) == mRaycastingObjectMap.end()); mRaycastingObjectMap[name] = body; mDynamicsWorld->addRigidBody(body,CollisionType_Raycasting,CollisionType_Raycasting|CollisionType_Projectile); + body->setCollisionFlags(body->getCollisionFlags() | btCollisionObject::CF_DISABLE_VISUALIZE_OBJECT); } return body; From 5ae1554a75cf1433e567355609346fedd1cb69fc Mon Sep 17 00:00:00 2001 From: cc9cii Date: Sun, 30 Nov 2014 04:00:06 +1100 Subject: [PATCH 135/303] Simplify skipping moved references (thanks scrawl) --- components/esm/loadcell.cpp | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/components/esm/loadcell.cpp b/components/esm/loadcell.cpp index f8966ad207..347f3fde4a 100644 --- a/components/esm/loadcell.cpp +++ b/components/esm/loadcell.cpp @@ -177,17 +177,10 @@ bool Cell::getNextRef(ESMReader &esm, CellRef &ref, bool& deleted) // NOTE: We should not need this check. It is a safety check until we have checked // more plugins, and how they treat these moved references. if (esm.isNextSub("MVRF")) { - esm.skipRecord(); // skip MVRF - esm.skipRecord(); // skip CNDT - // That should be it, I haven't seen any other fields yet. - } - - // If moved references are not handled then it is possible that CellRef::load() can lead - // to strange results because ESMReader::isNextSub("xxx") will always return false when - // there are no more subrecords. When moved references are handled properly by OpenCS - // below 2 lines can be removed. - if (!esm.hasMoreSubs()) + // skip rest of cell record (moved references), they are handled elsewhere + esm.skipRecord(); // skip MVRF, CNDT return false; + } ref.load (esm); From 4a9d2038fac8983b23b74b1c4a49914d218a0b4e Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 30 Nov 2014 14:33:39 +0100 Subject: [PATCH 136/303] load land for non-base content files immediately --- apps/opencs/model/world/data.cpp | 13 ++++++++++++- apps/opencs/model/world/idcollection.hpp | 18 +++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 9cb0299c4c..87dbaa16d1 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -713,7 +713,18 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Stage::Messages& messages) case ESM::REC_PGRD: mPathgrids.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_LAND: + { + int index = mLand.load(*mReader, mBase); + + if (index!=-1 && !mBase) + mLand.getRecord (index).mModified.mLand->loadData ( + ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | ESM::Land::DATA_VCLR | + ESM::Land::DATA_VTEX); + + break; + } case ESM::REC_CELL: { diff --git a/apps/opencs/model/world/idcollection.hpp b/apps/opencs/model/world/idcollection.hpp index 0129ba3d8d..f00ea447aa 100644 --- a/apps/opencs/model/world/idcollection.hpp +++ b/apps/opencs/model/world/idcollection.hpp @@ -15,12 +15,15 @@ namespace CSMWorld public: - void load (ESM::ESMReader& reader, bool base); + /// \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 - void load (const ESXRecordT& record, bool base, int index = -2); + /// + /// \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. @@ -36,7 +39,7 @@ namespace CSMWorld } template - void IdCollection::load (ESM::ESMReader& reader, bool base) + int IdCollection::load (ESM::ESMReader& reader, bool base) { std::string id = reader.getHNOString ("NAME"); @@ -64,6 +67,8 @@ namespace CSMWorld record.mState = RecordBase::State_Deleted; this->setRecord (index, record); } + + return -1; } else { @@ -88,12 +93,12 @@ namespace CSMWorld index = newIndex; } - load (record, base, index); + return load (record, base, index); } } template - void IdCollection::load (const ESXRecordT& record, bool base, + int IdCollection::load (const ESXRecordT& record, bool base, int index) { if (index==-2) @@ -106,6 +111,7 @@ namespace CSMWorld record2.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; (base ? record2.mBase : record2.mModified) = record; + index = this->getSize(); this->appendRecord (record2); } else @@ -120,6 +126,8 @@ namespace CSMWorld this->setRecord (index, record2); } + + return index; } template From db17dbe3242938f3a1dc995c0924f4b4b149b59d Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 30 Nov 2014 18:04:18 +0100 Subject: [PATCH 137/303] don't store esm readers for non-base content files --- apps/opencs/model/world/data.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 87dbaa16d1..737376f047 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -678,9 +678,15 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Stage::Messages& messages) if (!mReader->hasMoreRecs()) { - // Don't delete the Reader yet. Some record types store a reference to the Reader to handle on-demand loading - boost::shared_ptr ptr(mReader); - mReaders.push_back(ptr); + 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. + boost::shared_ptr ptr(mReader); + mReaders.push_back(ptr); + } + mReader = 0; mDialogue = 0; From 2720e5ea9df4aa2f8582bbb341792c24159218e7 Mon Sep 17 00:00:00 2001 From: cc9cii Date: Mon, 1 Dec 2014 06:44:12 +1100 Subject: [PATCH 138/303] Remove PhysicsManager singleton and use shared_ptr instead. Resolves the issue where sometimes destructors were called in an unexpected sequence resulting in a crash while exiting the application. --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/editor.cpp | 3 +- apps/opencs/editor.hpp | 2 - apps/opencs/model/doc/document.cpp | 12 +- apps/opencs/model/doc/document.hpp | 9 ++ apps/opencs/view/doc/view.cpp | 3 - apps/opencs/view/doc/viewmanager.cpp | 6 +- apps/opencs/view/render/cell.cpp | 2 +- apps/opencs/view/render/cell.hpp | 6 +- apps/opencs/view/render/mousestate.cpp | 2 +- apps/opencs/view/render/mousestate.hpp | 3 +- apps/opencs/view/render/object.cpp | 2 +- apps/opencs/view/render/object.hpp | 7 +- .../view/render/pagedworldspacewidget.cpp | 2 +- .../view/render/unpagedworldspacewidget.cpp | 4 +- apps/opencs/view/render/worldspacewidget.cpp | 12 +- apps/opencs/view/render/worldspacewidget.hpp | 6 +- apps/opencs/view/world/physicsmanager.cpp | 110 ------------------ apps/opencs/view/world/physicsmanager.hpp | 54 --------- 19 files changed, 47 insertions(+), 200 deletions(-) delete mode 100644 apps/opencs/view/world/physicsmanager.cpp delete mode 100644 apps/opencs/view/world/physicsmanager.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 9d8dd89e1a..bdfefbb83f 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -68,7 +68,7 @@ opencs_units (view/world opencs_units_noqt (view/world subviews enumdelegate vartypedelegate recordstatusdelegate idtypedelegate datadisplaydelegate - scripthighlighter idvalidator dialoguecreator physicssystem physicsmanager + scripthighlighter idvalidator dialoguecreator physicssystem ) opencs_units (view/widget diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index bef83b8ac7..e756cb5dfc 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -21,7 +21,7 @@ CS::Editor::Editor (OgreInit::OgreInit& ogreInit) : mUserSettings (mCfgMgr), mOverlaySystem (0), mDocumentManager (mCfgMgr), - mViewManager (mDocumentManager), mPhysicsManager (0), + mViewManager (mDocumentManager), mIpcServerName ("org.openmw.OpenCS"), mServer(NULL), mClientSocket(NULL) { std::pair > config = readConfig(); @@ -34,7 +34,6 @@ CS::Editor::Editor (OgreInit::OgreInit& ogreInit) ogreInit.init ((mCfgMgr.getUserConfigPath() / "opencsOgre.log").string()); mOverlaySystem.reset (new CSVRender::OverlaySystem); - mPhysicsManager.reset (new CSVWorld::PhysicsManager); Bsa::registerResources (Files::Collections (config.first, !mFsStrict), config.second, true, mFsStrict); diff --git a/apps/opencs/editor.hpp b/apps/opencs/editor.hpp index d55b0e873e..cd39d53a48 100644 --- a/apps/opencs/editor.hpp +++ b/apps/opencs/editor.hpp @@ -28,7 +28,6 @@ #include "view/settings/dialog.hpp" #include "view/render/overlaysystem.hpp" -#include "view/world/physicsmanager.hpp" namespace OgreInit { @@ -45,7 +44,6 @@ namespace CS Files::ConfigurationManager mCfgMgr; CSMSettings::UserSettings mUserSettings; std::auto_ptr mOverlaySystem; - std::auto_ptr mPhysicsManager; CSMDoc::DocumentManager mDocumentManager; CSVDoc::ViewManager mViewManager; CSVDoc::StartupDialogue mStartup; diff --git a/apps/opencs/model/doc/document.cpp b/apps/opencs/model/doc/document.cpp index 4fdfd2e5e4..4abd67a505 100644 --- a/apps/opencs/model/doc/document.cpp +++ b/apps/opencs/model/doc/document.cpp @@ -9,6 +9,8 @@ #include #endif +#include "../../view/world/physicssystem.hpp" + void CSMDoc::Document::addGmsts() { static const char *gmstFloats[] = @@ -2253,7 +2255,7 @@ CSMDoc::Document::Document (const Files::ConfigurationManager& configuration, mProjectPath ((configuration.getUserDataPath() / "projects") / (savePath.filename().string() + ".project")), mSaving (*this, mProjectPath, encoding), - mRunner (mProjectPath) + mRunner (mProjectPath), mPhysics(boost::shared_ptr()) { if (mContentFiles.empty()) throw std::runtime_error ("Empty content file sequence"); @@ -2464,3 +2466,11 @@ void CSMDoc::Document::progress (int current, int max, int type) { emit progress (current, max, type, 1, this); } + +boost::shared_ptr CSMDoc::Document::getPhysics () +{ + if(!mPhysics) + mPhysics = boost::shared_ptr (new CSVWorld::PhysicsSystem()); + + return mPhysics; +} diff --git a/apps/opencs/model/doc/document.hpp b/apps/opencs/model/doc/document.hpp index c5f6d10067..e5aa5eea58 100644 --- a/apps/opencs/model/doc/document.hpp +++ b/apps/opencs/model/doc/document.hpp @@ -3,6 +3,7 @@ #include +#include #include #include @@ -39,6 +40,11 @@ namespace CSMWorld class ResourcesManager; } +namespace CSVWorld +{ + class PhysicsSystem; +} + namespace CSMDoc { class Document : public QObject @@ -57,6 +63,7 @@ namespace CSMDoc boost::filesystem::path mResDir; Blacklist mBlacklist; Runner mRunner; + boost::shared_ptr mPhysics; // 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. @@ -129,6 +136,8 @@ namespace CSMDoc QTextDocument *getRunLog(); + boost::shared_ptr getPhysics(); + signals: void stateChanged (int state, CSMDoc::Document *document); diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index d64d36aec7..0d2b6060ef 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -16,7 +16,6 @@ #include "../../model/world/idtable.hpp" #include "../world/subviews.hpp" -#include "../world/physicsmanager.hpp" #include "../tools/subviews.hpp" @@ -407,8 +406,6 @@ CSVDoc::View::View (ViewManager& viewManager, CSMDoc::Document *document, int to mSubViewFactory.add (CSMWorld::UniversalId::Type_RunLog, new SubViewFactory); connect (mOperations, SIGNAL (abortOperation (int)), this, SLOT (abortOperation (int))); - - CSVWorld::PhysicsManager::instance()->setupPhysics(document); } CSVDoc::View::~View() diff --git a/apps/opencs/view/doc/viewmanager.cpp b/apps/opencs/view/doc/viewmanager.cpp index c4fd668843..55fd38c182 100644 --- a/apps/opencs/view/doc/viewmanager.cpp +++ b/apps/opencs/view/doc/viewmanager.cpp @@ -6,6 +6,8 @@ #include #include +#include + #include "../../model/doc/documentmanager.hpp" #include "../../model/doc/document.hpp" #include "../../model/world/columns.hpp" @@ -16,7 +18,6 @@ #include "../world/vartypedelegate.hpp" #include "../world/recordstatusdelegate.hpp" #include "../world/idtypedelegate.hpp" -#include "../world/physicsmanager.hpp" #include "../../model/settings/usersettings.hpp" @@ -219,7 +220,8 @@ void CSVDoc::ViewManager::removeDocAndView (CSMDoc::Document *document) mDocumentManager.removeDocument(document); (*iter)->deleteLater(); mViews.erase (iter); - CSVWorld::PhysicsManager::instance()->removeDocument(document); + // cleanup global resources used by OEngine + delete OEngine::Physic::BulletShapeManager::getSingletonPtr(); updateIndices(); return; diff --git a/apps/opencs/view/render/cell.cpp b/apps/opencs/view/render/cell.cpp index 75e11cc10a..1fb7809be1 100644 --- a/apps/opencs/view/render/cell.cpp +++ b/apps/opencs/view/render/cell.cpp @@ -60,7 +60,7 @@ bool CSVRender::Cell::addObjects (int start, int end) } CSVRender::Cell::Cell (CSMWorld::Data& data, Ogre::SceneManager *sceneManager, - const std::string& id, CSVWorld::PhysicsSystem *physics, const Ogre::Vector3& origin) + const std::string& id, boost::shared_ptr physics, const Ogre::Vector3& origin) : mData (data), mId (Misc::StringUtils::lowerCase (id)), mSceneMgr(sceneManager), mPhysics(physics) { mCellNode = sceneManager->getRootSceneNode()->createChildSceneNode(); diff --git a/apps/opencs/view/render/cell.hpp b/apps/opencs/view/render/cell.hpp index d38f0c68d1..9f38b0d9f3 100644 --- a/apps/opencs/view/render/cell.hpp +++ b/apps/opencs/view/render/cell.hpp @@ -5,6 +5,8 @@ #include #include +#include + #include #include @@ -38,7 +40,7 @@ namespace CSVRender Ogre::SceneNode *mCellNode; std::map mObjects; std::auto_ptr mTerrain; - CSVWorld::PhysicsSystem *mPhysics; + boost::shared_ptr mPhysics; Ogre::SceneManager *mSceneMgr; int mX; int mY; @@ -56,7 +58,7 @@ namespace CSVRender public: Cell (CSMWorld::Data& data, Ogre::SceneManager *sceneManager, const std::string& id, - CSVWorld::PhysicsSystem *physics, const Ogre::Vector3& origin = Ogre::Vector3 (0, 0, 0)); + boost::shared_ptr physics, const Ogre::Vector3& origin = Ogre::Vector3 (0, 0, 0)); ~Cell(); diff --git a/apps/opencs/view/render/mousestate.cpp b/apps/opencs/view/render/mousestate.cpp index 45e846f74a..988819fcb1 100644 --- a/apps/opencs/view/render/mousestate.cpp +++ b/apps/opencs/view/render/mousestate.cpp @@ -56,7 +56,7 @@ namespace CSVRender // MouseState::MouseState(WorldspaceWidget *parent) - : mParent(parent), mPhysics(parent->getPhysics()), mSceneManager(parent->getSceneManager()) + : mParent(parent), mPhysics(parent->mDocument.getPhysics()), mSceneManager(parent->getSceneManager()) , mCurrentObj(""), mMouseState(Mouse_Default), mOldPos(0,0), mMouseEventTimer(0), mPlane(0) , mGrabbedSceneNode(""), mOrigObjPos(Ogre::Vector3()), mOrigMousePos(Ogre::Vector3()) , mCurrentMousePos(Ogre::Vector3()), mOffset(0.0f) diff --git a/apps/opencs/view/render/mousestate.hpp b/apps/opencs/view/render/mousestate.hpp index 27907bb331..70e18427f3 100644 --- a/apps/opencs/view/render/mousestate.hpp +++ b/apps/opencs/view/render/mousestate.hpp @@ -2,6 +2,7 @@ #define OPENCS_VIEW_MOUSESTATE_H #include +#include #include #include @@ -43,7 +44,7 @@ namespace CSVRender MouseStates mMouseState; WorldspaceWidget *mParent; - CSVWorld::PhysicsSystem *mPhysics; // local copy + boost::shared_ptr mPhysics; Ogre::SceneManager *mSceneManager; // local copy QPoint mOldPos; diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp index 21219db8f4..af3777d0c8 100644 --- a/apps/opencs/view/render/object.cpp +++ b/apps/opencs/view/render/object.cpp @@ -132,7 +132,7 @@ const CSMWorld::CellRef& CSVRender::Object::getReference() const } CSVRender::Object::Object (const CSMWorld::Data& data, Ogre::SceneNode *cellNode, - const std::string& id, bool referenceable, CSVWorld::PhysicsSystem *physics, + const std::string& id, bool referenceable, boost::shared_ptr physics, bool forceBaseToZero) : mData (data), mBase (0), mForceBaseToZero (forceBaseToZero), mPhysics(physics) { diff --git a/apps/opencs/view/render/object.hpp b/apps/opencs/view/render/object.hpp index eba2dc8148..05a32fbeab 100644 --- a/apps/opencs/view/render/object.hpp +++ b/apps/opencs/view/render/object.hpp @@ -1,6 +1,8 @@ #ifndef OPENCS_VIEW_OBJECT_H #define OPENCS_VIEW_OBJECT_H +#include + #include class QModelIndex; @@ -31,7 +33,7 @@ namespace CSVRender Ogre::SceneNode *mBase; NifOgre::ObjectScenePtr mObject; bool mForceBaseToZero; - CSVWorld::PhysicsSystem *mPhysics; + boost::shared_ptr mPhysics; /// Not implemented Object (const Object&); @@ -58,7 +60,8 @@ namespace CSVRender Object (const CSMWorld::Data& data, Ogre::SceneNode *cellNode, const std::string& id, bool referenceable, - CSVWorld::PhysicsSystem *physics = NULL, bool forceBaseToZero = false); + boost::shared_ptr physics = boost::shared_ptr (), + bool forceBaseToZero = false); /// \param forceBaseToZero If this is a reference ignore the coordinates and place /// it at 0, 0, 0 instead. diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index e5d5428581..bae0fcacf4 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -111,7 +111,7 @@ bool CSVRender::PagedWorldspaceWidget::adjustCells() mCells.find (*iter)==mCells.end()) { Cell *cell = new Cell (mDocument.getData(), getSceneManager(), - iter->getId (mWorldspace), getPhysics()); + iter->getId (mWorldspace), mDocument.getPhysics()); mCells.insert (std::make_pair (*iter, cell)); float height = cell->getTerrainHeightAt(Ogre::Vector3( diff --git a/apps/opencs/view/render/unpagedworldspacewidget.cpp b/apps/opencs/view/render/unpagedworldspacewidget.cpp index 07acbe493c..3f8a455481 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.cpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.cpp @@ -49,7 +49,7 @@ CSVRender::UnpagedWorldspaceWidget::UnpagedWorldspaceWidget (const std::string& update(); - mCell.reset (new Cell (document.getData(), getSceneManager(), mCellId, getPhysics())); + mCell.reset (new Cell (document.getData(), getSceneManager(), mCellId, document.getPhysics())); } void CSVRender::UnpagedWorldspaceWidget::cellDataChanged (const QModelIndex& topLeft, @@ -91,7 +91,7 @@ bool CSVRender::UnpagedWorldspaceWidget::handleDrop (const std::vectorgetId(); - mCell.reset (new Cell (getDocument().getData(), getSceneManager(), mCellId, getPhysics())); + mCell.reset (new Cell (getDocument().getData(), getSceneManager(), mCellId, getDocument().getPhysics())); update(); emit cellChanged(*data.begin()); diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 1f7c415122..9ea582f3da 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -16,7 +16,6 @@ #include "../widget/scenetooltoggle2.hpp" #include "../widget/scenetoolrun.hpp" -#include "../world/physicsmanager.hpp" #include "../world/physicssystem.hpp" #include "elements.hpp" @@ -56,9 +55,7 @@ CSVRender::WorldspaceWidget::WorldspaceWidget (CSMDoc::Document& document, QWidg connect (debugProfiles, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (debugProfileAboutToBeRemoved (const QModelIndex&, int, int))); - // associate WorldSpaceWidgets (and their SceneManagers) with Documents - // then create physics if there is a new document - mPhysics = CSVWorld::PhysicsManager::instance()->addSceneWidget(document, this); + mPhysics = document.getPhysics(); // create physics if one doesn't exist mPhysics->addSceneManager(getSceneManager(), this); mMouse = new MouseState(this); } @@ -67,7 +64,6 @@ CSVRender::WorldspaceWidget::~WorldspaceWidget () { delete mMouse; mPhysics->removeSceneManager(getSceneManager()); - CSVWorld::PhysicsManager::instance()->removeSceneWidget(this); } void CSVRender::WorldspaceWidget::selectNavigationMode (const std::string& mode) @@ -370,12 +366,6 @@ void CSVRender::WorldspaceWidget::updateOverlay() { } -CSVWorld::PhysicsSystem *CSVRender::WorldspaceWidget::getPhysics() -{ - assert(mPhysics); - return mPhysics; -} - void CSVRender::WorldspaceWidget::mouseMoveEvent (QMouseEvent *event) { if(event->buttons() & Qt::RightButton) diff --git a/apps/opencs/view/render/worldspacewidget.hpp b/apps/opencs/view/render/worldspacewidget.hpp index 550b5d4a9a..b19197e36b 100644 --- a/apps/opencs/view/render/worldspacewidget.hpp +++ b/apps/opencs/view/render/worldspacewidget.hpp @@ -1,6 +1,8 @@ #ifndef OPENCS_VIEW_WORLDSPACEWIDGET_H #define OPENCS_VIEW_WORLDSPACEWIDGET_H +#include + #include "scenewidget.hpp" #include "mousestate.hpp" @@ -40,7 +42,7 @@ namespace CSVRender CSVWidget::SceneToolToggle2 *mSceneElements; CSVWidget::SceneToolRun *mRun; CSMDoc::Document& mDocument; - CSVWorld::PhysicsSystem *mPhysics; + boost::shared_ptr mPhysics; MouseState *mMouse; unsigned int mInteractionMask; @@ -115,8 +117,6 @@ namespace CSVRender virtual void updateOverlay(); - CSVWorld::PhysicsSystem *getPhysics(); - virtual void mouseMoveEvent (QMouseEvent *event); virtual void mousePressEvent (QMouseEvent *event); virtual void mouseReleaseEvent (QMouseEvent *event); diff --git a/apps/opencs/view/world/physicsmanager.cpp b/apps/opencs/view/world/physicsmanager.cpp deleted file mode 100644 index fa8db9e1e0..0000000000 --- a/apps/opencs/view/world/physicsmanager.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#include "physicsmanager.hpp" - -#include - -#include - -#include "../render/worldspacewidget.hpp" -#include "physicssystem.hpp" - -namespace CSVWorld -{ - PhysicsManager *PhysicsManager::mPhysicsManagerInstance = 0; - - PhysicsManager::PhysicsManager() - { - assert(!mPhysicsManagerInstance); - mPhysicsManagerInstance = this; - } - - PhysicsManager::~PhysicsManager() - { - std::map::iterator iter = mPhysics.begin(); - for(; iter != mPhysics.end(); ++iter) - delete iter->second; // shouldn't be any left but just in case - } - - PhysicsManager *PhysicsManager::instance() - { - assert(mPhysicsManagerInstance); - return mPhysicsManagerInstance; - } - - // create a physics instance per document, called from CSVDoc::View() to get Document* - void PhysicsManager::setupPhysics(CSMDoc::Document *doc) - { - std::map >::iterator iter = mSceneWidgets.find(doc); - if(iter == mSceneWidgets.end()) - { - mSceneWidgets[doc] = std::list (); // zero elements - mPhysics[doc] = new PhysicsSystem(); - } - } - - // destroy physics, called from CSVDoc::ViewManager - void PhysicsManager::removeDocument(CSMDoc::Document *doc) - { - std::map::iterator iter = mPhysics.find(doc); - if(iter != mPhysics.end()) - { - delete iter->second; - mPhysics.erase(iter); - } - - std::map >::iterator it = mSceneWidgets.find(doc); - if(it != mSceneWidgets.end()) - { - mSceneWidgets.erase(it); - } - - // cleanup global resources used by OEngine - if(mPhysics.empty()) - { - delete OEngine::Physic::BulletShapeManager::getSingletonPtr(); - } - } - - // called from CSVRender::WorldspaceWidget() to get widgets' association with Document& - PhysicsSystem *PhysicsManager::addSceneWidget(CSMDoc::Document &doc, CSVRender::WorldspaceWidget *widget) - { - CSVRender::SceneWidget *sceneWidget = static_cast(widget); - - std::map >::iterator iter = mSceneWidgets.begin(); - for(; iter != mSceneWidgets.end(); ++iter) - { - if((*iter).first == &doc) - { - (*iter).second.push_back(sceneWidget); - return mPhysics[(*iter).first]; // TODO: consider using shared_ptr instead - } - } - - throw std::runtime_error("No physics system found for the given document."); - } - - // deprecated by removeDocument() and may be deleted in future code updates - // however there may be some value in removing the deleted scene widgets from the - // list so that the list does not grow forever - void PhysicsManager::removeSceneWidget(CSVRender::WorldspaceWidget *widget) - { - CSVRender::SceneWidget *sceneWidget = static_cast(widget); - - std::map >::iterator iter = mSceneWidgets.begin(); - for(; iter != mSceneWidgets.end(); ++iter) - { - std::list::iterator itWidget = (*iter).second.begin(); - for(; itWidget != (*iter).second.end(); ++itWidget) - { - if((*itWidget) == sceneWidget) - { - (*iter).second.erase(itWidget); - - //if((*iter).second.empty()) // last one for the document - // NOTE: do not delete physics until the document itself is closed - - break; - } - } - } - } -} diff --git a/apps/opencs/view/world/physicsmanager.hpp b/apps/opencs/view/world/physicsmanager.hpp deleted file mode 100644 index e17c9ac84a..0000000000 --- a/apps/opencs/view/world/physicsmanager.hpp +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef CSV_WORLD_PHYSICSMANAGER_H -#define CSV_WORLD_PHYSICSMANAGER_H - -#include -#include - -namespace Ogre -{ - class SceneManager; -} - -namespace CSMDoc -{ - class Document; -} - -namespace CSVRender -{ - class WorldspaceWidget; - class SceneWidget; -} - -namespace CSVWorld -{ - class PhysicsSystem; -} - -namespace CSVWorld -{ - class PhysicsManager - { - static PhysicsManager *mPhysicsManagerInstance; - - std::map > mSceneWidgets; - std::map mPhysics; - - public: - - PhysicsManager(); - ~PhysicsManager(); - - static PhysicsManager *instance(); - - void setupPhysics(CSMDoc::Document *); - - PhysicsSystem *addSceneWidget(CSMDoc::Document &doc, CSVRender::WorldspaceWidget *widget); - - void removeSceneWidget(CSVRender::WorldspaceWidget *widget); - - void removeDocument(CSMDoc::Document *doc); - }; -} - -#endif // CSV_WORLD_PHYSICSMANAGER_H From a4e32d23c6b6377a813a3029fa7fbfcd1eeb3a9a Mon Sep 17 00:00:00 2001 From: scrawl Date: Sun, 30 Nov 2014 20:52:34 +0100 Subject: [PATCH 139/303] Fix old profile content not being cleared correctly when saving content file selection (Fixes #2173) --- apps/launcher/datafilespage.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 17951f2e49..f45b444707 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -137,6 +137,7 @@ void Launcher::DataFilesPage::saveSettings(const QString &profile) void Launcher::DataFilesPage::removeProfile(const QString &profile) { mLauncherSettings.remove(QString("Profiles/") + profile); + mLauncherSettings.remove(QString("Profiles/") + profile + QString("/content")); } QAbstractItemModel *Launcher::DataFilesPage::profilesModel() const From dffa8c6c149d1c90e9acde23a27d25d689fa38a7 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sun, 30 Nov 2014 22:02:25 +0100 Subject: [PATCH 140/303] Re-insert existing DialInfo records when they are modified by another content file (Fixes #2170) --- components/esm/loaddial.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/components/esm/loaddial.cpp b/components/esm/loaddial.cpp index ff0362aa21..f2da8f3775 100644 --- a/components/esm/loaddial.cpp +++ b/components/esm/loaddial.cpp @@ -63,6 +63,8 @@ void Dialogue::readInfo(ESMReader &esm, bool merge) std::map::iterator lookup; lookup = mLookup.find(id); + + ESM::DialInfo info; if (lookup != mLookup.end()) { it = lookup->second; @@ -70,13 +72,17 @@ void Dialogue::readInfo(ESMReader &esm, bool merge) // Merge with existing record. Only the subrecords that are present in // the new record will be overwritten. it->load(esm); - return; - } + info = *it; - // New record - ESM::DialInfo info; - info.mId = id; - info.load(esm); + // Since the record merging may have changed the next/prev linked list connection, we need to re-insert the record + mInfo.erase(it); + mLookup.erase(lookup); + } + else + { + info.mId = id; + info.load(esm); + } if (info.mNext.empty()) { From 9fb4b1f499689c77cdeb76c6021dfd2fb5fcb105 Mon Sep 17 00:00:00 2001 From: cc9cii Date: Mon, 1 Dec 2014 08:15:17 +1100 Subject: [PATCH 141/303] Initialise null shared_ptr --- apps/opencs/view/render/worldspacewidget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 9ea582f3da..879200b5dc 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -22,7 +22,7 @@ #include "editmode.hpp" CSVRender::WorldspaceWidget::WorldspaceWidget (CSMDoc::Document& document, QWidget* parent) -: SceneWidget (parent), mDocument(document), mSceneElements(0), mRun(0), mPhysics(0), mMouse(0), +: SceneWidget (parent), mDocument(document), mSceneElements(0), mRun(0), mPhysics(boost::shared_ptr()), mMouse(0), mInteractionMask (0) { setAcceptDrops(true); From 2d229c70cb163637c0057f67021a01eab8b872ad Mon Sep 17 00:00:00 2001 From: cc9cii Date: Mon, 1 Dec 2014 09:41:03 +1100 Subject: [PATCH 142/303] Another missed null shared_ptr conversion for gcc. --- apps/opencs/view/render/previewwidget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/opencs/view/render/previewwidget.cpp b/apps/opencs/view/render/previewwidget.cpp index f972c6361b..da18e7c895 100644 --- a/apps/opencs/view/render/previewwidget.cpp +++ b/apps/opencs/view/render/previewwidget.cpp @@ -10,7 +10,7 @@ CSVRender::PreviewWidget::PreviewWidget (CSMWorld::Data& data, const std::string& id, bool referenceable, QWidget *parent) : SceneWidget (parent), mData (data), - mObject (data, getSceneManager()->getRootSceneNode(), id, referenceable, NULL, true) + mObject (data, getSceneManager()->getRootSceneNode(), id, referenceable, boost::shared_ptr(), true) { setNavigation (&mOrbit); From 44b11163d18dd43b7e8bc3e1203049373480476f Mon Sep 17 00:00:00 2001 From: cc9cii Date: Mon, 1 Dec 2014 10:07:02 +1100 Subject: [PATCH 143/303] Do not delete physics objects if it was never created (e.g. preview window) --- apps/opencs/view/render/object.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp index af3777d0c8..d92b4aaa2d 100644 --- a/apps/opencs/view/render/object.cpp +++ b/apps/opencs/view/render/object.cpp @@ -156,7 +156,8 @@ CSVRender::Object::~Object() { clear(); - mPhysics->removeObject(mBase->getName()); + if(mPhysics) // preview may not have physics enabled + mPhysics->removeObject(mBase->getName()); if (mBase) mBase->getCreator()->destroySceneNode (mBase); From 64e1594b4179688e79a5fa01193239af73c9174a Mon Sep 17 00:00:00 2001 From: cc9cii Date: Mon, 1 Dec 2014 14:08:27 +1100 Subject: [PATCH 144/303] Move the destruction of global resources, being used by multiple documents, to the editor. --- apps/opencs/editor.cpp | 7 ++++++- apps/opencs/view/doc/viewmanager.cpp | 4 ---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index e756cb5dfc..53c6865eb4 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -1,6 +1,8 @@ #include "editor.hpp" +#include + #include #include #include @@ -69,7 +71,10 @@ CS::Editor::Editor (OgreInit::OgreInit& ogreInit) } CS::Editor::~Editor () -{} +{ + // cleanup global resources used by OEngine + delete OEngine::Physic::BulletShapeManager::getSingletonPtr(); +} void CS::Editor::setupDataFiles (const Files::PathContainer& dataDirs) { diff --git a/apps/opencs/view/doc/viewmanager.cpp b/apps/opencs/view/doc/viewmanager.cpp index 55fd38c182..5f6b6b46a4 100644 --- a/apps/opencs/view/doc/viewmanager.cpp +++ b/apps/opencs/view/doc/viewmanager.cpp @@ -6,8 +6,6 @@ #include #include -#include - #include "../../model/doc/documentmanager.hpp" #include "../../model/doc/document.hpp" #include "../../model/world/columns.hpp" @@ -220,8 +218,6 @@ void CSVDoc::ViewManager::removeDocAndView (CSMDoc::Document *document) mDocumentManager.removeDocument(document); (*iter)->deleteLater(); mViews.erase (iter); - // cleanup global resources used by OEngine - delete OEngine::Physic::BulletShapeManager::getSingletonPtr(); updateIndices(); return; From 3b5cd286f6ee51b50baf136040f4033ad589ad81 Mon Sep 17 00:00:00 2001 From: cc9cii Date: Mon, 1 Dec 2014 14:09:22 +1100 Subject: [PATCH 145/303] Do not destroy overlay if it was never created (e.g. due to an Ogre exception). --- apps/opencs/view/render/pagedworldspacewidget.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index bae0fcacf4..e7954491f2 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -362,8 +362,11 @@ CSVRender::PagedWorldspaceWidget::~PagedWorldspaceWidget() delete iter->second; } - removeRenderTargetListener(mOverlayMask); - delete mOverlayMask; + if(mOverlayMask) + { + removeRenderTargetListener(mOverlayMask); + delete mOverlayMask; + } } void CSVRender::PagedWorldspaceWidget::useViewHint (const std::string& hint) From 6e1a11f3220761c5ac808019d78eb69c4560ec67 Mon Sep 17 00:00:00 2001 From: MiroslavR Date: Mon, 1 Dec 2014 19:13:04 +0100 Subject: [PATCH 146/303] Queue screen fade operations invoked by scripts --- apps/openmw/mwbase/windowmanager.hpp | 6 +++--- apps/openmw/mwgui/windowmanagerimp.cpp | 15 +++++++++------ apps/openmw/mwgui/windowmanagerimp.hpp | 6 +++--- apps/openmw/mwscript/miscextensions.cpp | 6 +++--- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index bfc4f3b333..d4f1afa32d 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -330,11 +330,11 @@ namespace MWBase virtual void pinWindow (MWGui::GuiWindow window) = 0; /// Fade the screen in, over \a time seconds - virtual void fadeScreenIn(const float time) = 0; + virtual void fadeScreenIn(const float time, bool clearQueue=true) = 0; /// Fade the screen out to black, over \a time seconds - virtual void fadeScreenOut(const float time) = 0; + virtual void fadeScreenOut(const float time, bool clearQueue=true) = 0; /// Fade the screen to a specified percentage of black, over \a time seconds - virtual void fadeScreenTo(const int percent, const float time) = 0; + virtual void fadeScreenTo(const int percent, const float time, bool clearQueue=true) = 0; /// Darken the screen to a specified percentage virtual void setBlindness(const int percent) = 0; diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 6ab8b94c5c..48f28d300c 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -1748,21 +1748,24 @@ namespace MWGui updateVisible(); } - void WindowManager::fadeScreenIn(const float time) + void WindowManager::fadeScreenIn(const float time, bool clearQueue) { - mScreenFader->clearQueue(); + if (clearQueue) + mScreenFader->clearQueue(); mScreenFader->fadeOut(time); } - void WindowManager::fadeScreenOut(const float time) + void WindowManager::fadeScreenOut(const float time, bool clearQueue) { - mScreenFader->clearQueue(); + if (clearQueue) + mScreenFader->clearQueue(); mScreenFader->fadeIn(time); } - void WindowManager::fadeScreenTo(const int percent, const float time) + void WindowManager::fadeScreenTo(const int percent, const float time, bool clearQueue) { - mScreenFader->clearQueue(); + if (clearQueue) + mScreenFader->clearQueue(); mScreenFader->fadeTo(percent, time); } diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index aa5bd0fc91..94d8a93db2 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -325,11 +325,11 @@ namespace MWGui virtual void pinWindow (MWGui::GuiWindow window); /// Fade the screen in, over \a time seconds - virtual void fadeScreenIn(const float time); + virtual void fadeScreenIn(const float time, bool clearQueue); /// Fade the screen out to black, over \a time seconds - virtual void fadeScreenOut(const float time); + virtual void fadeScreenOut(const float time, bool clearQueue); /// Fade the screen to a specified percentage of black, over \a time seconds - virtual void fadeScreenTo(const int percent, const float time); + virtual void fadeScreenTo(const int percent, const float time, bool clearQueue); /// Darken the screen to a specified percentage virtual void setBlindness(const int percent); diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index e8784f8de2..aa80213de8 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -246,7 +246,7 @@ namespace MWScript Interpreter::Type_Float time = runtime[0].mFloat; runtime.pop(); - MWBase::Environment::get().getWindowManager()->fadeScreenIn(time); + MWBase::Environment::get().getWindowManager()->fadeScreenIn(time, false); } }; @@ -259,7 +259,7 @@ namespace MWScript Interpreter::Type_Float time = runtime[0].mFloat; runtime.pop(); - MWBase::Environment::get().getWindowManager()->fadeScreenOut(time); + MWBase::Environment::get().getWindowManager()->fadeScreenOut(time, false); } }; @@ -275,7 +275,7 @@ namespace MWScript Interpreter::Type_Float time = runtime[0].mFloat; runtime.pop(); - MWBase::Environment::get().getWindowManager()->fadeScreenTo(alpha, time); + MWBase::Environment::get().getWindowManager()->fadeScreenTo(alpha, time, false); } }; From 48d5789aeb94c5916395b16929438350146ba09f Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 1 Dec 2014 22:10:06 +0100 Subject: [PATCH 147/303] Use a separate flag for references deleted by a content file (Fixes #2018) The flag must be separate so as to not contaminate the user's savegame. Fixes the following use cases that were broken before: - Content file edits a reference that was already deleted by a previously loaded content file -> reference must stay deleted - Changed or new content file deletes a reference that is already present in the user's savegame -> reference must be deleted - Said content file is disabled again - reference must be undeleted --- apps/openmw/mwworld/cellstore.cpp | 2 +- apps/openmw/mwworld/cellstore.hpp | 2 +- apps/openmw/mwworld/localscripts.cpp | 2 +- apps/openmw/mwworld/refdata.cpp | 19 ++++++++++++++++--- apps/openmw/mwworld/refdata.hpp | 11 ++++++++++- apps/openmw/mwworld/scene.cpp | 2 +- apps/openmw/mwworld/worldimp.cpp | 2 +- 7 files changed, 31 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index ce8d77758c..52e70fef26 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -156,7 +156,7 @@ namespace MWWorld LiveRef liveCellRef (ref, ptr); if (deleted) - liveCellRef.mData.setCount (0); + liveCellRef.mData.setDeleted(true); if (iter != mList.end()) *iter = liveCellRef; diff --git a/apps/openmw/mwworld/cellstore.hpp b/apps/openmw/mwworld/cellstore.hpp index e322ef4a40..05e7b0b2e0 100644 --- a/apps/openmw/mwworld/cellstore.hpp +++ b/apps/openmw/mwworld/cellstore.hpp @@ -196,7 +196,7 @@ namespace MWWorld for (typename List::List::iterator iter (list.mList.begin()); iter!=list.mList.end(); ++iter) { - if (!iter->mData.getCount()) + if (iter->mData.isDeleted()) continue; if (!functor (MWWorld::Ptr(&*iter, this))) return false; diff --git a/apps/openmw/mwworld/localscripts.cpp b/apps/openmw/mwworld/localscripts.cpp index f3a6471249..d74aab6943 100644 --- a/apps/openmw/mwworld/localscripts.cpp +++ b/apps/openmw/mwworld/localscripts.cpp @@ -17,7 +17,7 @@ namespace cellRefList.mList.begin()); iter!=cellRefList.mList.end(); ++iter) { - if (!iter->mBase->mScript.empty() && iter->mData.getCount()) + if (!iter->mBase->mScript.empty() && !iter->mData.isDeleted()) { localScripts.add (iter->mBase->mScript, MWWorld::Ptr (&*iter, cell)); } diff --git a/apps/openmw/mwworld/refdata.cpp b/apps/openmw/mwworld/refdata.cpp index f4bc64b708..78fea2b868 100644 --- a/apps/openmw/mwworld/refdata.cpp +++ b/apps/openmw/mwworld/refdata.cpp @@ -23,6 +23,7 @@ namespace MWWorld mPosition = refData.mPosition; mLocalRotation = refData.mLocalRotation; mChanged = refData.mChanged; + mDeleted = refData.mDeleted; mCustomData = refData.mCustomData ? refData.mCustomData->clone() : 0; } @@ -36,7 +37,7 @@ namespace MWWorld } RefData::RefData() - : mBaseNode(0), mHasLocals (false), mEnabled (true), mCount (1), mCustomData (0), mChanged(false) + : mBaseNode(0), mHasLocals (false), mEnabled (true), mCount (1), mCustomData (0), mChanged(false), mDeleted(false) { for (int i=0; i<3; ++i) { @@ -49,7 +50,8 @@ namespace MWWorld RefData::RefData (const ESM::CellRef& cellRef) : mBaseNode(0), mHasLocals (false), mEnabled (true), mCount (1), mPosition (cellRef.mPos), mCustomData (0), - mChanged(false) // Loading from ESM/ESP files -> assume unchanged + mChanged(false), // Loading from ESM/ESP files -> assume unchanged + mDeleted(false) { mLocalRotation.rot[0]=0; mLocalRotation.rot[1]=0; @@ -59,7 +61,8 @@ namespace MWWorld RefData::RefData (const ESM::ObjectState& objectState) : mBaseNode (0), mHasLocals (false), mEnabled (objectState.mEnabled), mCount (objectState.mCount), mPosition (objectState.mPosition), mCustomData (0), - mChanged(true) // Loading from a savegame -> assume changed + mChanged(true), // Loading from a savegame -> assume changed + mDeleted(false) { for (int i=0; i<3; ++i) mLocalRotation.rot[i] = objectState.mLocalRotation[i]; @@ -167,6 +170,16 @@ namespace MWWorld mCount = count; } + void RefData::setDeleted(bool deleted) + { + mDeleted = deleted; + } + + bool RefData::isDeleted() const + { + return mDeleted || mCount == 0; + } + MWScript::Locals& RefData::getLocals() { return mLocals; diff --git a/apps/openmw/mwworld/refdata.hpp b/apps/openmw/mwworld/refdata.hpp index db66c091bb..1ed3cd79d9 100644 --- a/apps/openmw/mwworld/refdata.hpp +++ b/apps/openmw/mwworld/refdata.hpp @@ -37,6 +37,8 @@ namespace MWWorld bool mEnabled; int mCount; // 0: deleted + bool mDeleted; // separate delete flag used for deletion by a content file + ESM::Position mPosition; LocalRotation mLocalRotation; @@ -86,12 +88,19 @@ namespace MWWorld void setLocals (const ESM::Script& script); void setCount (int count); - /// Set object count (an object pile is a simple object with a count >1). + ///< 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 setDeleted(bool deleted); + + /// Returns true if the object was either deleted by the content file or by gameplay. + bool isDeleted() const; + MWScript::Locals& getLocals(); bool isEnabled() const; diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 6f18a6ef3a..02c9db9ea4 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -75,7 +75,7 @@ namespace ptr.getCellRef().setScale(2); } - if (ptr.getRefData().getCount() && ptr.getRefData().isEnabled()) + if (!ptr.getRefData().isDeleted() && ptr.getRefData().isEnabled()) { try { diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 4c2bd669b6..7baf1d3e5c 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1057,7 +1057,7 @@ namespace MWWorld void World::deleteObject (const Ptr& ptr) { - if (ptr.getRefData().getCount() > 0) + if (!ptr.getRefData().isDeleted()) { ptr.getRefData().setCount(0); From cbcd6a26d568d6a4b161fc2f849cba701bc4bef1 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Mon, 1 Dec 2014 22:57:32 +0100 Subject: [PATCH 148/303] memory leak fix --- apps/opencs/model/world/data.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 737376f047..f6bd8e13b9 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -686,6 +686,8 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Stage::Messages& messages) boost::shared_ptr ptr(mReader); mReaders.push_back(ptr); } + else + delete mReader; mReader = 0; From 61d1aa78ce94c60414b31e1ac88614f35ebf8748 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 1 Dec 2014 13:34:42 +0100 Subject: [PATCH 149/303] Move AiWander path finder to temporary storage (Fixes #2082) --- apps/openmw/mwmechanics/aipackage.hpp | 1 + apps/openmw/mwmechanics/aiwander.cpp | 44 ++++++++++++++------------- apps/openmw/mwmechanics/aiwander.hpp | 5 +-- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index df970f8013..f1c9ec7d25 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -69,6 +69,7 @@ namespace MWMechanics /** \return If the actor has arrived at his destination **/ bool pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Point dest, float duration); + // TODO: all this does not belong here, move into temporary storage PathFinder mPathFinder; ObstacleCheck mObstacleCheck; diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index a70200833c..a5be250f74 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -57,6 +57,8 @@ namespace MWMechanics bool mWalking; unsigned short mPlayedIdle; + + PathFinder mPathFinder; AiWanderStorage(): mTargetAngle(0), @@ -211,9 +213,9 @@ namespace MWMechanics // Are we there yet? bool& chooseAction = storage.mChooseAction; if(walking && - mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2])) + storage.mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2])) { - stopWalking(actor); + stopWalking(actor, storage); moveNow = false; walking = false; chooseAction = true; @@ -225,7 +227,7 @@ namespace MWMechanics if(walking) // have not yet reached the destination { // turn towards the next point in mPath - zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]))); + zTurn(actor, Ogre::Degree(storage.mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]))); actor.getClass().getMovementSettings(actor).mPosition[1] = 1; // Returns true if evasive action needs to be taken @@ -236,9 +238,9 @@ namespace MWMechanics { // remove allowed points then select another random destination mTrimCurrentNode = true; - trimAllowedNodes(mAllowedNodes, mPathFinder); + trimAllowedNodes(mAllowedNodes, storage.mPathFinder); mObstacleCheck.clear(); - mPathFinder.clearPath(); + storage.mPathFinder.clearPath(); walking = false; moveNow = true; } @@ -249,7 +251,7 @@ namespace MWMechanics actor.getClass().getMovementSettings(actor).mPosition[0] = 1; actor.getClass().getMovementSettings(actor).mPosition[1] = 0.1f; // change the angle a bit, too - zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0] + 1, pos.pos[1]))); + zTurn(actor, Ogre::Degree(storage.mPathFinder.getZAngleToNext(pos.pos[0] + 1, pos.pos[1]))); } mStuckCount++; // TODO: maybe no longer needed } @@ -260,7 +262,7 @@ namespace MWMechanics //std::cout << "Reset \""<< cls.getName(actor) << "\"" << std::endl; mObstacleCheck.clear(); - stopWalking(actor); + stopWalking(actor, storage); moveNow = false; walking = false; chooseAction = true; @@ -300,7 +302,7 @@ namespace MWMechanics { if(!mRepeat) { - stopWalking(actor); + stopWalking(actor, storage); return true; } else @@ -310,7 +312,7 @@ namespace MWMechanics { if(!mRepeat) { - stopWalking(actor); + stopWalking(actor, storage); return true; } else @@ -411,7 +413,7 @@ namespace MWMechanics chooseAction = false; idleNow = false; - if (!mPathFinder.isPathConstructed()) + if (!storage.mPathFinder.isPathConstructed()) { Ogre::Vector3 destNodePos = mReturnPosition; @@ -427,9 +429,9 @@ namespace MWMechanics start.mZ = pos.pos[2]; // don't take shortcuts for wandering - mPathFinder.buildPath(start, dest, actor.getCell(), false); + storage.mPathFinder.buildPath(start, dest, actor.getCell(), false); - if(mPathFinder.isPathConstructed()) + if(storage.mPathFinder.isPathConstructed()) { moveNow = false; walking = true; @@ -517,7 +519,7 @@ namespace MWMechanics if(walking) { - stopWalking(actor); + stopWalking(actor, storage); moveNow = false; walking = false; mObstacleCheck.clear(); @@ -567,7 +569,7 @@ namespace MWMechanics if(moveNow && mDistance) { // Construct a new path if there isn't one - if(!mPathFinder.isPathConstructed()) + if(!storage.mPathFinder.isPathConstructed()) { assert(mAllowedNodes.size()); unsigned int randNode = (int)(rand() / ((double)RAND_MAX + 1) * mAllowedNodes.size()); @@ -589,16 +591,16 @@ namespace MWMechanics start.mZ = pos.pos[2]; // don't take shortcuts for wandering - mPathFinder.buildPath(start, dest, actor.getCell(), false); + storage.mPathFinder.buildPath(start, dest, actor.getCell(), false); - if(mPathFinder.isPathConstructed()) + if(storage.mPathFinder.isPathConstructed()) { // buildPath inserts dest in case it is not a pathgraph point // index which is a duplicate for AiWander. However below code // does not work since getPath() returns a copy of path not a // reference - //if(mPathFinder.getPathSize() > 1) - //mPathFinder.getPath().pop_back(); + //if(storage.mPathFinder.getPathSize() > 1) + //storage.mPathFinder.getPath().pop_back(); // Remove this node as an option and add back the previously used node (stops NPC from picking the same node): ESM::Pathgrid::Point temp = mAllowedNodes[randNode]; @@ -616,7 +618,7 @@ namespace MWMechanics // Choose a different node and delete this one from possible nodes because it is uncreachable: else mAllowedNodes.erase(mAllowedNodes.begin() + randNode); - } + } } return false; // AiWander package not yet completed @@ -653,9 +655,9 @@ namespace MWMechanics return TypeIdWander; } - void AiWander::stopWalking(const MWWorld::Ptr& actor) + void AiWander::stopWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage) { - mPathFinder.clearPath(); + storage.mPathFinder.clearPath(); actor.getClass().getMovementSettings(actor).mPosition[1] = 0; } diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 0600909bae..b9b394a264 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -27,6 +27,8 @@ namespace MWMechanics { + struct AiWanderStorage; + /// \brief Causes the Actor to wander within a specified range class AiWander : public AiPackage { @@ -65,7 +67,7 @@ namespace MWMechanics // NOTE: mDistance and mDuration must be set already void init(); - void stopWalking(const MWWorld::Ptr& actor); + void stopWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage); void playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); void getRandomIdle(unsigned short& playedIdle); @@ -101,7 +103,6 @@ namespace MWMechanics void trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder); -// PathFinder mPathFinder; // ObstacleCheck mObstacleCheck; float mDoorCheckDuration; From 6960cac5eb279562c7791a92e3b7bb9af93a00f3 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 1 Dec 2014 13:38:47 +0100 Subject: [PATCH 150/303] Disable third person zoom feature by default due to usability issues (Fixes #2129) --- apps/openmw/mwinput/inputmanagerimp.cpp | 4 +++- files/settings-default.cfg | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 68682abeac..48acd22ba3 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -623,7 +623,9 @@ namespace MWInput if (arg.zrel && mControlSwitch["playerviewswitch"] && mControlSwitch["playercontrols"]) //Check to make sure you are allowed to zoomout and there is a change { MWBase::Environment::get().getWorld()->changeVanityModeScale(arg.zrel); - MWBase::Environment::get().getWorld()->setCameraDistance(arg.zrel, true, true); + + if (Settings::Manager::getBool("allow third person zoom", "Input")) + MWBase::Environment::get().getWorld()->setCameraDistance(arg.zrel, true, true); } } } diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 12b52d3db6..7566994e29 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -179,6 +179,8 @@ camera y multiplier = 1.0 always run = false +allow third person zoom = false + [Game] # Always use the most powerful attack when striking with a weapon (chop, slash or thrust) best attack = false From c684c99a95f3462ee96926af8a8eafbbf3b39975 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 1 Dec 2014 13:43:11 +0100 Subject: [PATCH 151/303] Combat AI: Don't attempt to cast spells when impossible to succeed (Fixes #2059) --- apps/openmw/mwmechanics/aicombataction.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index cc8279b1eb..6b4ede3059 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -9,6 +9,7 @@ #include "../mwworld/actionequip.hpp" #include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/spellcasting.hpp" #include #include @@ -166,6 +167,9 @@ namespace MWMechanics { const CreatureStats& stats = actor.getClass().getCreatureStats(actor); + if (MWMechanics::getSpellSuccessChance(spell, actor) == 0) + return 0.f; + if (spell->mData.mType != ESM::Spell::ST_Spell) return 0.f; From 077c619611ace83df33ced9066931d8086b55f89 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 1 Dec 2014 14:36:36 +0100 Subject: [PATCH 152/303] Implement Clamp mode for NiTexturingProperty (Fixes #2050) --- components/nifogre/material.cpp | 30 ++++++++++++++++++++++++++---- files/materials/objects.mat | 4 ++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/components/nifogre/material.cpp b/components/nifogre/material.cpp index 517f29f4e3..04eb86ba6f 100644 --- a/components/nifogre/material.cpp +++ b/components/nifogre/material.cpp @@ -54,6 +54,28 @@ static const char *getTestMode(int mode) return "less_equal"; } +static void setTextureProperties(sh::MaterialInstance* material, const std::string& textureSlotName, const Nif::NiTexturingProperty::Texture& tex) +{ + material->setProperty(textureSlotName + "UVSet", sh::makeProperty(new sh::IntValue(tex.uvSet))); + const std::string clampMode = textureSlotName + "ClampMode"; + switch (tex.clamp) + { + case 0: + material->setProperty(clampMode, sh::makeProperty(new sh::StringValue("clamp clamp"))); + break; + case 1: + material->setProperty(clampMode, sh::makeProperty(new sh::StringValue("clamp wrap"))); + break; + case 2: + material->setProperty(clampMode, sh::makeProperty(new sh::StringValue("wrap clamp"))); + break; + case 3: + default: + material->setProperty(clampMode, sh::makeProperty(new sh::StringValue("wrap wrap"))); + break; + } +} + Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, const Ogre::String &name, const Ogre::String &group, const Nif::NiTexturingProperty *texprop, @@ -294,22 +316,22 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, if (!texName[Nif::NiTexturingProperty::BaseTexture].empty()) { instance->setProperty("use_diffuse_map", sh::makeProperty(new sh::BooleanValue(true))); - instance->setProperty("diffuseMapUVSet", sh::makeProperty(new sh::IntValue(texprop->textures[Nif::NiTexturingProperty::BaseTexture].uvSet))); + setTextureProperties(instance, "diffuseMap", texprop->textures[Nif::NiTexturingProperty::BaseTexture]); } if (!texName[Nif::NiTexturingProperty::GlowTexture].empty()) { instance->setProperty("use_emissive_map", sh::makeProperty(new sh::BooleanValue(true))); - instance->setProperty("emissiveMapUVSet", sh::makeProperty(new sh::IntValue(texprop->textures[Nif::NiTexturingProperty::GlowTexture].uvSet))); + setTextureProperties(instance, "emissiveMap", texprop->textures[Nif::NiTexturingProperty::GlowTexture]); } if (!texName[Nif::NiTexturingProperty::DetailTexture].empty()) { instance->setProperty("use_detail_map", sh::makeProperty(new sh::BooleanValue(true))); - instance->setProperty("detailMapUVSet", sh::makeProperty(new sh::IntValue(texprop->textures[Nif::NiTexturingProperty::DetailTexture].uvSet))); + setTextureProperties(instance, "detailMap", texprop->textures[Nif::NiTexturingProperty::DetailTexture]); } if (!texName[Nif::NiTexturingProperty::DarkTexture].empty()) { instance->setProperty("use_dark_map", sh::makeProperty(new sh::BooleanValue(true))); - instance->setProperty("darkMapUVSet", sh::makeProperty(new sh::IntValue(texprop->textures[Nif::NiTexturingProperty::DarkTexture].uvSet))); + setTextureProperties(instance, "darkMap", texprop->textures[Nif::NiTexturingProperty::DarkTexture]); } bool useParallax = !texName[Nif::NiTexturingProperty::BumpTexture].empty() diff --git a/files/materials/objects.mat b/files/materials/objects.mat index 932c7e25f2..149160760b 100644 --- a/files/materials/objects.mat +++ b/files/materials/objects.mat @@ -73,6 +73,7 @@ material openmw_objects_base direct_texture $diffuseMap create_in_ffp $use_diffuse_map tex_coord_set $diffuseMapUVSet + tex_address_mode $diffuseMapClampMode } texture_unit normalMap @@ -89,6 +90,7 @@ material openmw_objects_base alpha_op_ex modulate src_current src_texture direct_texture $darkMap tex_coord_set $darkMapUVSet + tex_address_mode $darkMapClampMode } texture_unit detailMap @@ -97,6 +99,7 @@ material openmw_objects_base colour_op_ex modulate_x2 src_current src_texture direct_texture $detailMap tex_coord_set $detailMapUVSet + tex_address_mode $detailMapClampMode } texture_unit emissiveMap @@ -105,6 +108,7 @@ material openmw_objects_base colour_op add direct_texture $emissiveMap tex_coord_set $emissiveMapUVSet + tex_address_mode $emissiveMapClampMode } texture_unit envMap From 59cde9b4314234b22d8b24f8993035692b8222bf Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 1 Dec 2014 14:38:50 +0100 Subject: [PATCH 153/303] Don't use transparency override if there's no transparency (rug fix for Bug #2050) --- components/nifogre/material.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/nifogre/material.cpp b/components/nifogre/material.cpp index 04eb86ba6f..072f16344b 100644 --- a/components/nifogre/material.cpp +++ b/components/nifogre/material.cpp @@ -354,7 +354,7 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, instance->setProperty("has_vertex_colour", sh::makeProperty(new sh::BooleanValue(true))); // Override alpha flags based on our override list (transparency-overrides.cfg) - if (!texName[0].empty()) + if ((alphaFlags&1) && !texName[0].empty()) { NifOverrides::TransparencyResult result = NifOverrides::Overrides::getTransparencyOverride(texName[0]); if (result.first) From 8103d25b09710f1d4fa7d7bec727d0bec60ece93 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 1 Dec 2014 14:48:53 +0100 Subject: [PATCH 154/303] Make ToggleMenus close open windows (Fixes #2045) --- apps/openmw/mwscript/guiextensions.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/openmw/mwscript/guiextensions.cpp b/apps/openmw/mwscript/guiextensions.cpp index afc745beb9..1d34adbca0 100644 --- a/apps/openmw/mwscript/guiextensions.cpp +++ b/apps/openmw/mwscript/guiextensions.cpp @@ -210,6 +210,12 @@ namespace MWScript { bool state = MWBase::Environment::get().getWindowManager()->toggleGui(); 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(); + } } }; From f9ae0d9d665b7ad08327017e417cc4572b0b48d7 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 1 Dec 2014 14:56:13 +0100 Subject: [PATCH 155/303] Fix dialogue goodbye link conflicting with choice links --- apps/openmw/mwgui/dialogue.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 6526b200f9..eb548d596d 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -533,9 +533,11 @@ namespace MWGui if (mGoodbye) { + Goodbye* link = new Goodbye(); + mLinks.push_back(link); std::string goodbye = MWBase::Environment::get().getWorld()->getStore().get().find("sGoodbye")->getString(); BookTypesetter::Style* questionStyle = typesetter->createHotStyle(body, linkNormal, linkHot, linkActive, - TypesetBook::InteractiveId(mLinks.back())); + TypesetBook::InteractiveId(link)); typesetter->lineBreak(); typesetter->write(questionStyle, to_utf8_span(goodbye.c_str())); } @@ -654,7 +656,6 @@ namespace MWGui void DialogueWindow::goodbye() { - mLinks.push_back(new Goodbye()); mGoodbye = true; mEnabled = false; updateHistory(); From a1226501fac2f1f25213ffad546d2565fede51fd Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 1 Dec 2014 15:08:55 +0100 Subject: [PATCH 156/303] AiWander: move idle animation handling to non-delayed section (Fixes #2073) --- apps/openmw/mwmechanics/aiwander.cpp | 105 ++++++++++++++------------- 1 file changed, 53 insertions(+), 52 deletions(-) diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index a5be250f74..c8a0c85d58 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -282,6 +282,58 @@ namespace MWMechanics rotate = false; } + // Check if idle animation finished + short unsigned& playedIdle = storage.mPlayedIdle; + GreetingState& greetingState = storage.mSaidGreeting; + if(idleNow && !checkIdle(actor, playedIdle) && (greetingState == Greet_Done || greetingState == Greet_None)) + { + playedIdle = 0; + idleNow = false; + chooseAction = true; + } + + MWBase::World *world = MWBase::Environment::get().getWorld(); + + if(chooseAction) + { + playedIdle = 0; + getRandomIdle(playedIdle); // NOTE: sets mPlayedIdle with a random selection + + if(!playedIdle && mDistance) + { + chooseAction = false; + moveNow = true; + } + else + { + // Play idle animation and recreate vanilla (broken?) behavior of resetting start time of AIWander: + MWWorld::TimeStamp currentTime = world->getTimeStamp(); + mStartTime = currentTime; + playIdle(actor, playedIdle); + chooseAction = false; + idleNow = true; + + // Play idle voiced dialogue entries randomly + int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified(); + if (hello > 0) + { + int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + + // Don't bother if the player is out of hearing range + static float fVoiceIdleOdds = MWBase::Environment::get().getWorld()->getStore() + .get().find("fVoiceIdleOdds")->getFloat(); + + // Only say Idle voices when player is in LOS + // A bit counterintuitive, likely vanilla did this to reduce the appearance of + // voices going through walls? + if (roll < fVoiceIdleOdds && Ogre::Vector3(player.getRefData().getPosition().pos).squaredDistance(Ogre::Vector3(pos.pos)) < 1500*1500 + && MWBase::Environment::get().getWorld()->getLOS(player, actor)) + MWBase::Environment::get().getDialogueManager()->say(actor, "idle"); + } + } + } + float& lastReaction = storage.mReaction; lastReaction += duration; if(lastReaction < REACTION_INTERVAL) @@ -293,7 +345,6 @@ namespace MWMechanics // NOTE: everything below get updated every REACTION_INTERVAL seconds - MWBase::World *world = MWBase::Environment::get().getWorld(); if(mDuration) { // End package if duration is complete or mid-night hits: @@ -439,48 +490,6 @@ namespace MWMechanics } } - AiWander::GreetingState& greetingState = storage.mSaidGreeting; - short unsigned& playedIdle = storage.mPlayedIdle; - if(chooseAction) - { - playedIdle = 0; - getRandomIdle(playedIdle); // NOTE: sets mPlayedIdle with a random selection - - if(!playedIdle && mDistance) - { - chooseAction = false; - moveNow = true; - } - else - { - // Play idle animation and recreate vanilla (broken?) behavior of resetting start time of AIWander: - MWWorld::TimeStamp currentTime = world->getTimeStamp(); - mStartTime = currentTime; - playIdle(actor, playedIdle); - chooseAction = false; - idleNow = true; - - // Play idle voiced dialogue entries randomly - int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified(); - if (hello > 0) - { - int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - - // Don't bother if the player is out of hearing range - static float fVoiceIdleOdds = MWBase::Environment::get().getWorld()->getStore() - .get().find("fVoiceIdleOdds")->getFloat(); - - // Only say Idle voices when player is in LOS - // A bit counterintuitive, likely vanilla did this to reduce the appearance of - // voices going through walls? - if (roll < fVoiceIdleOdds && Ogre::Vector3(player.getRefData().getPosition().pos).squaredDistance(Ogre::Vector3(pos.pos)) < 1500*1500 - && MWBase::Environment::get().getWorld()->getLOS(player, actor)) - MWBase::Environment::get().getDialogueManager()->say(actor, "idle"); - } - } - } - // Allow interrupting a walking actor to trigger a greeting if(idleNow || walking) { @@ -496,7 +505,7 @@ namespace MWMechanics Ogre::Vector3 playerPos(player.getRefData().getPosition().pos); Ogre::Vector3 actorPos(actor.getRefData().getPosition().pos); float playerDistSqr = playerPos.squaredDistance(actorPos); - + int& greetingTimer = storage.mGreetingTimer; if (greetingState == Greet_None) { @@ -556,14 +565,6 @@ namespace MWMechanics if (playerDistSqr >= fGreetDistanceReset*fGreetDistanceReset) greetingState = Greet_None; } - - // Check if idle animation finished - if(!checkIdle(actor, playedIdle) && (playerDistSqr > helloDistance*helloDistance || greetingState == MWMechanics::AiWander::Greet_Done)) - { - playedIdle = 0; - idleNow = false; - chooseAction = true; - } } if(moveNow && mDistance) From ed686ddd2f403dccd713aaa8c97aded49aca1631 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 1 Dec 2014 15:37:34 +0100 Subject: [PATCH 157/303] Don't update nodes with an empty name from the skeleton source (Fixes #2125) --- apps/openmw/mwrender/animation.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index fc147ce59a..548906cdf2 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -574,7 +574,8 @@ float Animation::getVelocity(const std::string &groupname) const static void updateBoneTree(const Ogre::SkeletonInstance *skelsrc, Ogre::Bone *bone) { - if(skelsrc->hasBone(bone->getName())) + if(bone->getName() != " " // really should be != "", but see workaround in skeleton.cpp for empty node names + && skelsrc->hasBone(bone->getName())) { Ogre::Bone *srcbone = skelsrc->getBone(bone->getName()); if(!srcbone->getParent() || !bone->getParent()) From 507cbcfae320a02e7b2af2651faac4571f09b5e6 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 1 Dec 2014 16:04:00 +0100 Subject: [PATCH 158/303] Remove incorrect implementation of the iAlarm* GMSTs, not used by vanilla MW (Fixes #2064) According to Hrnchamd, these are unused. The real mechanics are not fully documented, but from a quick test only NPCs with an alarm value of 100 will report crimes. --- apps/openmw/mwmechanics/mechanicsmanagerimp.cpp | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index f7949f1ece..b242327476 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -930,19 +930,6 @@ namespace MWMechanics const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); - // What amount of alarm did this crime generate? - int alarm = 0; - if (type == OT_Trespassing || type == OT_SleepingInOwnedBed) - alarm = esmStore.get().find("iAlarmTresspass")->getInt(); - else if (type == OT_Pickpocket) - alarm = esmStore.get().find("iAlarmPickPocket")->getInt(); - else if (type == OT_Assault) - alarm = esmStore.get().find("iAlarmAttack")->getInt(); - else if (type == OT_Murder) - alarm = esmStore.get().find("iAlarmKilling")->getInt(); - else if (type == OT_Theft) - alarm = esmStore.get().find("iAlarmStealing")->getInt(); - bool reported = false; // Find all the actors within the alarm radius @@ -988,7 +975,7 @@ namespace MWMechanics continue; // Will the witness report the crime? - if (it->getClass().getCreatureStats(*it).getAiSetting(CreatureStats::AI_Alarm).getBase() >= alarm) + if (it->getClass().getCreatureStats(*it).getAiSetting(CreatureStats::AI_Alarm).getBase() >= 100) { reported = true; } From 46d93f1b08984c234a090a4207f40e8480c4147a Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 1 Dec 2014 16:36:01 +0100 Subject: [PATCH 159/303] Crime update: NPCs can report crimes if they didn't see the crime, but were alerted by someone who saw it and did not report it themselves. --- apps/openmw/mwmechanics/mechanicsmanagerimp.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index b242327476..b4edf44aa9 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -930,7 +930,6 @@ namespace MWMechanics const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); - bool reported = false; // Find all the actors within the alarm radius std::vector neighbors; @@ -947,6 +946,8 @@ namespace MWMechanics bool victimAware = false; // Find actors who directly witnessed the crime + bool crimeSeen = false; + bool reported = false; for (std::vector::iterator it = neighbors.begin(); it != neighbors.end(); ++it) { if (*it == player) @@ -974,15 +975,17 @@ namespace MWMechanics if (it->getClass().getCreatureStats(*it).getAiSequence().isInCombat(victim)) continue; - // Will the witness report the crime? - if (it->getClass().getCreatureStats(*it).getAiSetting(CreatureStats::AI_Alarm).getBase() >= 100) - { - reported = true; - } + crimeSeen = true; + } + + // Will the witness report the crime? + if (it->getClass().getCreatureStats(*it).getAiSetting(CreatureStats::AI_Alarm).getBase() >= 100) + { + reported = true; } } - if (reported) + if (crimeSeen && reported) reportCrime(player, victim, type, arg); else if (victimAware && !victim.isEmpty() && type == OT_Assault) startCombat(victim, player); From b9d0552166c7ad283b166d38731aaf5b1875c64e Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 1 Dec 2014 16:43:55 +0100 Subject: [PATCH 160/303] Fix positionCell rotation argument when used on the player This fixes the player's initial orientation on the starting boat, to properly face Jiub. --- .../mwscript/transformationextensions.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp index e74388effc..8e6d925b7c 100644 --- a/apps/openmw/mwscript/transformationextensions.cpp +++ b/apps/openmw/mwscript/transformationextensions.cpp @@ -319,12 +319,11 @@ namespace MWScript ptr = MWWorld::Ptr(ptr.getBase(), store); float ax = Ogre::Radian(ptr.getRefData().getPosition().rot[0]).valueDegrees(); float ay = Ogre::Radian(ptr.getRefData().getPosition().rot[1]).valueDegrees(); - if(ptr.getTypeName() == typeid(ESM::NPC).name())//some morrowind oddity - { - ax = ax/60.; - ay = ay/60.; + // 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 != MWBase::Environment::get().getWorld()->getPlayerPtr()) zRot = zRot/60.; - } MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,zRot); ptr.getClass().adjustPosition(ptr, false); @@ -378,12 +377,11 @@ namespace MWScript float ax = Ogre::Radian(ptr.getRefData().getPosition().rot[0]).valueDegrees(); float ay = Ogre::Radian(ptr.getRefData().getPosition().rot[1]).valueDegrees(); - if(ptr.getTypeName() == typeid(ESM::NPC).name())//some morrowind oddity - { - ax = ax/60.; - ay = ay/60.; + // 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 != MWBase::Environment::get().getWorld()->getPlayerPtr()) zRot = zRot/60.; - } MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,zRot); ptr.getClass().adjustPosition(ptr, false); } From fadbb5ad2196418c558abf977b3f2db0ed87489e Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 1 Dec 2014 20:31:33 +0100 Subject: [PATCH 161/303] Add particle and sound fading for weather transitions (Fixes #2130) --- apps/openmw/mwrender/sky.cpp | 40 ++++++++++++++++ apps/openmw/mwworld/weather.cpp | 82 ++++++++++++++++++--------------- apps/openmw/mwworld/weather.hpp | 16 ++++--- 3 files changed, 94 insertions(+), 44 deletions(-) diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index ccef74efb4..1841021279 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -34,6 +34,41 @@ using namespace MWRender; using namespace Ogre; +namespace +{ + +void setAlpha (NifOgre::ObjectScenePtr scene, Ogre::MovableObject* movable, float alpha) +{ + Ogre::MaterialPtr mat = scene->mMaterialControllerMgr.getWritableMaterial(movable); + Ogre::Material::TechniqueIterator techs = mat->getTechniqueIterator(); + while(techs.hasMoreElements()) + { + Ogre::Technique *tech = techs.getNext(); + Ogre::Technique::PassIterator passes = tech->getPassIterator(); + while(passes.hasMoreElements()) + { + Ogre::Pass *pass = passes.getNext(); + Ogre::ColourValue diffuse = pass->getDiffuse(); + diffuse.a = alpha; + pass->setDiffuse(diffuse); + } + } + +} + +void setAlpha (NifOgre::ObjectScenePtr scene, float alpha) +{ + for(size_t i = 0; i < scene->mParticles.size(); ++i) + setAlpha(scene, scene->mParticles[i], alpha); + for(size_t i = 0; i < scene->mEntities.size(); ++i) + { + if (scene->mEntities[i] != scene->mSkelBase) + setAlpha(scene, scene->mEntities[i], alpha); + } +} + +} + BillboardObject::BillboardObject( const String& textureName, const float initialSize, const Vector3& position, @@ -660,6 +695,11 @@ void SkyManager::setWeather(const MWWorld::WeatherResult& weather) mSun->setVisibility(weather.mGlareView * strength); mAtmosphereNight->setVisible(weather.mNight && mEnabled); + + if (mParticle.get()) + setAlpha(mParticle, weather.mEffectFade); + for (std::map::iterator it = mRainModels.begin(); it != mRainModels.end(); ++it) + setAlpha(it->second, weather.mEffectFade); } void SkyManager::setGlare(const float glare) diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index f738734b11..3f9b7d5623 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -6,6 +6,8 @@ #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwsound/sound.hpp" + #include "../mwrender/renderingmanager.hpp" #include "player.hpp" @@ -152,12 +154,12 @@ WeatherManager::WeatherManager(MWRender::RenderingManager* rendering,MWWorld::Fa setFallbackWeather(foggy,"foggy"); Weather thunderstorm; - thunderstorm.mRainLoopSoundID = "rain heavy"; + thunderstorm.mAmbientLoopSoundID = "rain heavy"; thunderstorm.mRainEffect = "meshes\\raindrop.nif"; setFallbackWeather(thunderstorm,"thunderstorm"); Weather rain; - rain.mRainLoopSoundID = "rain"; + rain.mAmbientLoopSoundID = "rain"; rain.mRainEffect = "meshes\\raindrop.nif"; setFallbackWeather(rain,"rain"); @@ -186,7 +188,7 @@ WeatherManager::WeatherManager(MWRender::RenderingManager* rendering,MWWorld::Fa WeatherManager::~WeatherManager() { - stopSounds(true); + stopSounds(); } void WeatherManager::setWeather(const String& weather, bool instant) @@ -228,6 +230,8 @@ void WeatherManager::setResult(const String& weatherType) mResult.mCloudSpeed = current.mCloudSpeed; mResult.mGlareView = current.mGlareView; mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; + mResult.mAmbientSoundVolume = 1.f; + mResult.mEffectFade = 1.f; mResult.mSunColor = current.mSunDiscSunsetColor; mResult.mIsStorm = current.mIsStorm; @@ -341,11 +345,30 @@ void WeatherManager::transition(float factor) mResult.mNight = current.mNight; - mResult.mIsStorm = current.mIsStorm; - mResult.mParticleEffect = current.mParticleEffect; - mResult.mRainEffect = current.mRainEffect; - mResult.mRainSpeed = current.mRainSpeed; - mResult.mRainFrequency = current.mRainFrequency; + if (factor < 0.5) + { + mResult.mIsStorm = current.mIsStorm; + mResult.mParticleEffect = current.mParticleEffect; + mResult.mRainEffect = current.mRainEffect; + mResult.mParticleEffect = current.mParticleEffect; + mResult.mRainSpeed = current.mRainSpeed; + mResult.mRainFrequency = current.mRainFrequency; + mResult.mAmbientSoundVolume = 1-(factor*2); + mResult.mEffectFade = mResult.mAmbientSoundVolume; + mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; + } + else + { + mResult.mIsStorm = other.mIsStorm; + mResult.mParticleEffect = other.mParticleEffect; + mResult.mRainEffect = other.mRainEffect; + mResult.mParticleEffect = other.mParticleEffect; + mResult.mRainSpeed = other.mRainSpeed; + mResult.mRainFrequency = other.mRainFrequency; + mResult.mAmbientSoundVolume = 2*(factor-0.5); + mResult.mEffectFade = mResult.mAmbientSoundVolume; + mResult.mAmbientLoopSoundID = other.mAmbientLoopSoundID; + } } void WeatherManager::update(float duration, bool paused) @@ -361,7 +384,7 @@ void WeatherManager::update(float duration, bool paused) { mRendering->skyDisable(); mRendering->getSkyManager()->setLightningStrength(0.f); - stopSounds(true); + stopSounds(); return; } @@ -541,40 +564,25 @@ void WeatherManager::update(float duration, bool paused) mRendering->getSkyManager()->setWeather(mResult); // Play sounds - if (mNextWeather == "") + if (mPlayingSoundID != mResult.mAmbientLoopSoundID) { - std::string ambientSnd = mWeatherSettings[mCurrentWeather].mAmbientLoopSoundID; - if (!ambientSnd.empty() && std::find(mSoundsPlaying.begin(), mSoundsPlaying.end(), ambientSnd) == mSoundsPlaying.end()) - { - mSoundsPlaying.push_back(ambientSnd); - MWBase::Environment::get().getSoundManager()->playSound(ambientSnd, 1.0, 1.0, MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_Loop); - } + stopSounds(); + if (!mResult.mAmbientLoopSoundID.empty()) + mAmbientSound = MWBase::Environment::get().getSoundManager()->playSound(mResult.mAmbientLoopSoundID, 1.0, 1.0, MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_Loop); - std::string rainSnd = mWeatherSettings[mCurrentWeather].mRainLoopSoundID; - if (!rainSnd.empty() && std::find(mSoundsPlaying.begin(), mSoundsPlaying.end(), rainSnd) == mSoundsPlaying.end()) - { - mSoundsPlaying.push_back(rainSnd); - MWBase::Environment::get().getSoundManager()->playSound(rainSnd, 1.0, 1.0, MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_Loop); - } + mPlayingSoundID = mResult.mAmbientLoopSoundID; } - - stopSounds(false); + if (mAmbientSound.get()) + mAmbientSound->setVolume(mResult.mAmbientSoundVolume); } -void WeatherManager::stopSounds(bool stopAll) +void WeatherManager::stopSounds() { - std::vector::iterator it = mSoundsPlaying.begin(); - while (it!=mSoundsPlaying.end()) + if (mAmbientSound.get()) { - if (stopAll || - !((*it == mWeatherSettings[mCurrentWeather].mAmbientLoopSoundID) || - (*it == mWeatherSettings[mCurrentWeather].mRainLoopSoundID))) - { - MWBase::Environment::get().getSoundManager()->stopSound(*it); - it = mSoundsPlaying.erase(it); - } - else - ++it; + MWBase::Environment::get().getSoundManager()->stopSound(mAmbientSound); + mAmbientSound.reset(); + mPlayingSoundID.clear(); } } @@ -764,7 +772,7 @@ bool WeatherManager::readRecord(ESM::ESMReader& reader, int32_t type) state.load(reader); // reset other temporary state, now that we loaded successfully - stopSounds(true); // let's hope this never throws + stopSounds(); // let's hope this never throws mRegionOverrides.clear(); mRegionMods.clear(); mThunderFlash = 0.0; diff --git a/apps/openmw/mwworld/weather.hpp b/apps/openmw/mwworld/weather.hpp index 97897fda92..dee9fc52ae 100644 --- a/apps/openmw/mwworld/weather.hpp +++ b/apps/openmw/mwworld/weather.hpp @@ -7,6 +7,8 @@ #include #include +#include "../mwbase/soundmanager.hpp" + namespace ESM { struct Region; @@ -61,10 +63,12 @@ namespace MWWorld bool mIsStorm; std::string mAmbientLoopSoundID; + float mAmbientSoundVolume; std::string mParticleEffect; - std::string mRainEffect; + float mEffectFade; + float mRainSpeed; float mRainFrequency; }; @@ -125,9 +129,6 @@ namespace MWWorld // This is used for Blight, Ashstorm and Blizzard (Bloodmoon) std::string mAmbientLoopSoundID; - // Rain sound effect - std::string mRainLoopSoundID; - // 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) @@ -173,7 +174,7 @@ namespace MWWorld */ void update(float duration, bool paused = false); - void stopSounds(bool stopAll); + void stopSounds(); void setHour(const float hour); @@ -206,6 +207,9 @@ namespace MWWorld bool mIsStorm; Ogre::Vector3 mStormDirection; + MWBase::SoundPtr mAmbientSound; + std::string mPlayingSoundID; + MWWorld::Fallback* mFallback; void setFallbackWeather(Weather& weather,const std::string& name); MWRender::RenderingManager* mRendering; @@ -214,8 +218,6 @@ namespace MWWorld std::map mRegionOverrides; - std::vector mSoundsPlaying; - std::string mCurrentWeather; std::string mNextWeather; From 406cf2b9814cbce5202871103ce7d4dfff1b55e2 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 2 Dec 2014 11:17:39 +0100 Subject: [PATCH 162/303] disable element visibility buttons that do not apply to the respective cell type --- apps/opencs/view/render/pagedworldspacewidget.cpp | 9 +++++++++ apps/opencs/view/render/pagedworldspacewidget.hpp | 2 ++ apps/opencs/view/render/unpagedworldspacewidget.cpp | 8 ++++++++ apps/opencs/view/render/unpagedworldspacewidget.hpp | 4 ++++ apps/opencs/view/render/worldspacewidget.cpp | 2 -- apps/opencs/view/widget/scenetooltoggle2.cpp | 7 +++++-- apps/opencs/view/widget/scenetooltoggle2.hpp | 2 +- 7 files changed, 29 insertions(+), 5 deletions(-) diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index e5d5428581..764320a703 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -22,6 +22,7 @@ #include "../widget/scenetooltoggle.hpp" #include "../widget/scenetoolmode.hpp" +#include "../widget/scenetooltoggle2.hpp" #include "editmode.hpp" #include "elements.hpp" @@ -212,6 +213,14 @@ void CSVRender::PagedWorldspaceWidget::mouseDoubleClickEvent (QMouseEvent *event WorldspaceWidget::mouseDoubleClickEvent(event); } +void CSVRender::PagedWorldspaceWidget::addVisibilitySelectorButtons ( + CSVWidget::SceneToolToggle2 *tool) +{ + WorldspaceWidget::addVisibilitySelectorButtons (tool); + tool->addButton (Element_Terrain, "Terrain"); + tool->addButton (Element_Fog, "Fog", "", true); +} + void CSVRender::PagedWorldspaceWidget::addEditModeSelectorButtons ( CSVWidget::SceneToolMode *tool) { diff --git a/apps/opencs/view/render/pagedworldspacewidget.hpp b/apps/opencs/view/render/pagedworldspacewidget.hpp index ca618d1220..3db6ee4edb 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.hpp +++ b/apps/opencs/view/render/pagedworldspacewidget.hpp @@ -83,6 +83,8 @@ namespace CSVRender protected: + virtual void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle2 *tool); + virtual void addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool); virtual void updateOverlay(); diff --git a/apps/opencs/view/render/unpagedworldspacewidget.cpp b/apps/opencs/view/render/unpagedworldspacewidget.cpp index 07acbe493c..62cbdf1e1f 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.cpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.cpp @@ -153,6 +153,14 @@ void CSVRender::UnpagedWorldspaceWidget::referenceAdded (const QModelIndex& pare flagAsModified(); } +void CSVRender::UnpagedWorldspaceWidget::addVisibilitySelectorButtons ( + CSVWidget::SceneToolToggle2 *tool) +{ + WorldspaceWidget::addVisibilitySelectorButtons (tool); + tool->addButton (Element_Terrain, "Terrain", "", true); + tool->addButton (Element_Fog, "Fog"); +} + std::string CSVRender::UnpagedWorldspaceWidget::getStartupInstruction() { Ogre::Vector3 position = getCamera()->getPosition(); diff --git a/apps/opencs/view/render/unpagedworldspacewidget.hpp b/apps/opencs/view/render/unpagedworldspacewidget.hpp index 237cb8f46f..d01c3e7667 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.hpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.hpp @@ -60,6 +60,10 @@ namespace CSVRender virtual std::string getStartupInstruction(); + protected: + + virtual void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle2 *tool); + private slots: void cellDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 1f7c415122..51dfcd5066 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -263,10 +263,8 @@ void CSVRender::WorldspaceWidget::addVisibilitySelectorButtons ( CSVWidget::SceneToolToggle2 *tool) { tool->addButton (Element_Reference, "References"); - tool->addButton (Element_Terrain, "Terrain"); tool->addButton (Element_Water, "Water"); tool->addButton (Element_Pathgrid, "Pathgrid"); - tool->addButton (Element_Fog, "Fog"); } void CSVRender::WorldspaceWidget::addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool) diff --git a/apps/opencs/view/widget/scenetooltoggle2.cpp b/apps/opencs/view/widget/scenetooltoggle2.cpp index 1c5c11a4da..313e519cb4 100644 --- a/apps/opencs/view/widget/scenetooltoggle2.cpp +++ b/apps/opencs/view/widget/scenetooltoggle2.cpp @@ -72,7 +72,7 @@ void CSVWidget::SceneToolToggle2::showPanel (const QPoint& position) } void CSVWidget::SceneToolToggle2::addButton (unsigned int id, - const QString& name, const QString& tooltip) + const QString& name, const QString& tooltip, bool disabled) { std::ostringstream stream; stream << mSingleIcon << id; @@ -84,6 +84,9 @@ void CSVWidget::SceneToolToggle2::addButton (unsigned int id, button->setIconSize (QSize (mIconSize, mIconSize)); button->setFixedSize (mButtonSize, mButtonSize); + if (disabled) + button->setDisabled (true); + mLayout->addWidget (button); ButtonDesc desc; @@ -95,7 +98,7 @@ void CSVWidget::SceneToolToggle2::addButton (unsigned int id, connect (button, SIGNAL (clicked()), this, SLOT (selected())); - if (mButtons.size()==1) + if (mButtons.size()==1 && !disabled) mFirst = button; } diff --git a/apps/opencs/view/widget/scenetooltoggle2.hpp b/apps/opencs/view/widget/scenetooltoggle2.hpp index 4bd9ba26f6..0bae780f97 100644 --- a/apps/opencs/view/widget/scenetooltoggle2.hpp +++ b/apps/opencs/view/widget/scenetooltoggle2.hpp @@ -56,7 +56,7 @@ namespace CSVWidget /// \attention After the last button has been added, setSelection must be called at /// least once to finalise the layout. void addButton (unsigned int id, - const QString& name, const QString& tooltip = ""); + const QString& name, const QString& tooltip = "", bool disabled = false); unsigned int getSelection() const; From bfa048e687586af7829f2d3626fbf185ad20be34 Mon Sep 17 00:00:00 2001 From: Paulo Viadanna Date: Tue, 2 Dec 2014 12:42:01 -0200 Subject: [PATCH 163/303] Fix #1734: AI will stop combat if target disappear --- apps/openmw/mwmechanics/aicombat.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 67fd544566..6249606322 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -325,6 +325,11 @@ namespace MWMechanics currentAction = prepareNextAction(actor, target); actionCooldown = currentAction->getActionCooldown(); } + + // Stop attacking if target is not seen + if (!MWBase::Environment::get().getMechanicsManager()->awarenessCheck(target, actor)) + return true; + if (currentAction.get()) currentAction->getCombatRange(rangeAttack, rangeFollow); From 4e756a2f4a971c62fd9e756d8e6fc7c44f26f5fc Mon Sep 17 00:00:00 2001 From: root Date: Wed, 3 Dec 2014 01:03:27 +0400 Subject: [PATCH 164/303] path to game get through jni --- components/files/androidpath.h | 19 +++++++++++++++++++ components/files/androidpath.hpp | 7 ++++++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 components/files/androidpath.h diff --git a/components/files/androidpath.h b/components/files/androidpath.h new file mode 100644 index 0000000000..3157c067f5 --- /dev/null +++ b/components/files/androidpath.h @@ -0,0 +1,19 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include + +#ifndef _Included_org_libsdl_app_SDLActivity_getPathToJni +#define _Included_org_libsdl_app_SDLActivity_getPathToJni +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: Java_org_libsdl_app_SDLActivity_getPathToJni + * Method: getPathToJni + * Signature: (I)I + */ +JNIEXPORT void JNICALL Java_org_libsdl_app_SDLActivity_getPathToJni(JNIEnv *env, jobject obj, jstring prompt); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/components/files/androidpath.hpp b/components/files/androidpath.hpp index 792462fc65..9d47d6d947 100644 --- a/components/files/androidpath.hpp +++ b/components/files/androidpath.hpp @@ -7,12 +7,17 @@ /** * \namespace Files */ + + namespace Files { - +class getJniPath{ +public: const char *getPathFromJni; +}; struct AndroidPath { AndroidPath(const std::string& application_name); + /** * \brief Return path to the user directory. From 85b8fca1f0112a1ea1b84f929007e48c8a7a81a4 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 3 Dec 2014 01:11:50 +0400 Subject: [PATCH 165/303] fixes --- components/files/androidpath.cpp | 46 ++++++++++++++++++++++++++++---- components/files/androidpath.hpp | 4 +-- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/components/files/androidpath.cpp b/components/files/androidpath.cpp index e2f9e948a1..1176260952 100644 --- a/components/files/androidpath.cpp +++ b/components/files/androidpath.cpp @@ -5,11 +5,37 @@ #include #include #include +#include "androidpath.h" #include #include + +class Buffer { + public: + static void setData(char const *data); + static char const * getData(); +}; +static char const *path; + +void Buffer::setData(char const *data) +{ + path=data; +} +char const * Buffer::getData() +{ + return path; +} + + +JNIEXPORT void JNICALL Java_org_libsdl_app_SDLActivity_getPathToJni(JNIEnv *env, jobject obj, jstring prompt) +{ + jboolean iscopy; + Buffer::setData((env)->GetStringUTFChars(prompt, &iscopy)); +} + namespace { + boost::filesystem::path getUserHome() { const char* dir = getenv("HOME"); @@ -53,22 +79,30 @@ AndroidPath::AndroidPath(const std::string& application_name) boost::filesystem::path AndroidPath::getUserConfigPath() const { - return getEnv("XDG_CONFIG_HOME", "/sdcard/libopenmw/config") / mName; + std::string buffer = ""; + buffer = buffer + Buffer::getData() +"/config"; + return getEnv("XDG_CONFIG_HOME", buffer) / mName; } boost::filesystem::path AndroidPath::getUserDataPath() const { - return getEnv("XDG_DATA_HOME", "/sdcard/libopenmw/share") / mName; + std::string buffer = ""; + buffer = buffer + Buffer::getData() +"/share"; + return getEnv("XDG_DATA_HOME", buffer) / mName; } boost::filesystem::path AndroidPath::getCachePath() const { - return getEnv("XDG_CACHE_HOME", "/sdcard/libopenmw/cache") / mName; + std::string buffer = ""; + buffer = buffer + Buffer::getData() +"/cache"; + return getEnv("XDG_CACHE_HOME", buffer) / mName; } boost::filesystem::path AndroidPath::getGlobalConfigPath() const { - boost::filesystem::path globalPath("/sdcard/libopenmw/"); + std::string buffer = ""; + buffer = buffer + Buffer::getData() +"/"; + boost::filesystem::path globalPath(buffer); return globalPath / mName; } @@ -79,7 +113,9 @@ boost::filesystem::path AndroidPath::getLocalPath() const boost::filesystem::path AndroidPath::getGlobalDataPath() const { - boost::filesystem::path globalDataPath("/sdcard/libopenmw/data"); + std::string buffer = ""; + buffer = buffer + Buffer::getData() +"/data"; + boost::filesystem::path globalDataPath(buffer); return globalDataPath / mName; } diff --git a/components/files/androidpath.hpp b/components/files/androidpath.hpp index 9d47d6d947..a8124e6db9 100644 --- a/components/files/androidpath.hpp +++ b/components/files/androidpath.hpp @@ -11,9 +11,7 @@ namespace Files { -class getJniPath{ -public: const char *getPathFromJni; -}; + struct AndroidPath { AndroidPath(const std::string& application_name); From e755f692cc015c1700b6c6b479b3def7352a2626 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 3 Dec 2014 09:42:12 +0100 Subject: [PATCH 166/303] silenced some annoying warnings --- apps/wizard/componentselectionpage.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index 956f0f2379..1fcde78579 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -62,7 +62,7 @@ void Wizard::ComponentSelectionPage::initializePage() if (field(QLatin1String("installation.new")).toBool() == true) { - morrowindItem->setFlags(morrowindItem->flags() & ~Qt::ItemIsEnabled | Qt::ItemIsUserCheckable); + morrowindItem->setFlags((morrowindItem->flags() & ~Qt::ItemIsEnabled) | Qt::ItemIsUserCheckable); morrowindItem->setData(Qt::CheckStateRole, Qt::Checked); componentsList->addItem(morrowindItem); @@ -77,7 +77,7 @@ void Wizard::ComponentSelectionPage::initializePage() if (mWizard->mInstallations[path].hasMorrowind) { morrowindItem->setText(tr("Morrowind\t\t(installed)")); - morrowindItem->setFlags(morrowindItem->flags() & ~Qt::ItemIsEnabled | Qt::ItemIsUserCheckable); + morrowindItem->setFlags((morrowindItem->flags() & ~Qt::ItemIsEnabled) | Qt::ItemIsUserCheckable); morrowindItem->setData(Qt::CheckStateRole, Qt::Unchecked); } else { morrowindItem->setText(tr("Morrowind")); @@ -88,7 +88,7 @@ void Wizard::ComponentSelectionPage::initializePage() if (mWizard->mInstallations[path].hasTribunal) { tribunalItem->setText(tr("Tribunal\t\t(installed)")); - tribunalItem->setFlags(tribunalItem->flags() & ~Qt::ItemIsEnabled | Qt::ItemIsUserCheckable); + tribunalItem->setFlags((tribunalItem->flags() & ~Qt::ItemIsEnabled) | Qt::ItemIsUserCheckable); tribunalItem->setData(Qt::CheckStateRole, Qt::Unchecked); } else { tribunalItem->setText(tr("Tribunal")); @@ -99,7 +99,7 @@ void Wizard::ComponentSelectionPage::initializePage() if (mWizard->mInstallations[path].hasBloodmoon) { bloodmoonItem->setText(tr("Bloodmoon\t\t(installed)")); - bloodmoonItem->setFlags(bloodmoonItem->flags() & ~Qt::ItemIsEnabled | Qt::ItemIsUserCheckable); + bloodmoonItem->setFlags((bloodmoonItem->flags() & ~Qt::ItemIsEnabled) | Qt::ItemIsUserCheckable); bloodmoonItem->setData(Qt::CheckStateRole, Qt::Unchecked); } else { bloodmoonItem->setText(tr("Bloodmoon")); From 58b6e757e39708f016fd7b386cad8466ee42277f Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 3 Dec 2014 15:24:37 +0100 Subject: [PATCH 167/303] fixed another case folding problem regarding OpenCS resources handling --- apps/opencs/model/world/resources.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/opencs/model/world/resources.cpp b/apps/opencs/model/world/resources.cpp index 8e255bc964..aeef59fe7a 100644 --- a/apps/opencs/model/world/resources.cpp +++ b/apps/opencs/model/world/resources.cpp @@ -55,7 +55,8 @@ CSMWorld::Resources::Resources (const std::string& baseDirectory, UniversalId::T std::string file = iter->substr (baseSize+1); mFiles.push_back (file); - mIndex.insert (std::make_pair (file, static_cast (mFiles.size())-1)); + mIndex.insert (std::make_pair ( + Misc::StringUtils::lowerCase (file), static_cast (mFiles.size())-1)); } } } From f2d991505ef97c80c1667256ed6a641351885c05 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 3 Dec 2014 15:31:00 +0100 Subject: [PATCH 168/303] handle other Windows-specific path issues regarding OpenCS resources handling --- apps/opencs/model/world/resources.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/opencs/model/world/resources.cpp b/apps/opencs/model/world/resources.cpp index aeef59fe7a..13c8df84d1 100644 --- a/apps/opencs/model/world/resources.cpp +++ b/apps/opencs/model/world/resources.cpp @@ -3,6 +3,7 @@ #include #include +#include #include @@ -55,6 +56,7 @@ CSMWorld::Resources::Resources (const std::string& baseDirectory, UniversalId::T std::string file = iter->substr (baseSize+1); mFiles.push_back (file); + std::replace (file.begin(), file.end(), '\\', '/'); mIndex.insert (std::make_pair ( Misc::StringUtils::lowerCase (file), static_cast (mFiles.size())-1)); } @@ -90,6 +92,8 @@ 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()) From 329a3558bfb073d670aa05ec88490e3b153fbe29 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 3 Dec 2014 15:36:07 +0100 Subject: [PATCH 169/303] updated credits file --- credits.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/credits.txt b/credits.txt index c6a5c71696..76efb2760a 100644 --- a/credits.txt +++ b/credits.txt @@ -96,6 +96,7 @@ terrorfisch Thomas Luppi (Digmaster) Tom Mason (wheybags) Torben Leif Carrington (TorbenC) +viadanna Vincent Heuken vocollapse From dd0cea21b0ead17a13198f3e584d343c6bb31875 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 1 Dec 2014 23:25:25 +0100 Subject: [PATCH 170/303] Implement overwriting pathgrid records (Fixes #2175) --- apps/openmw/mwworld/store.hpp | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index 55c5b8191f..469b93f88e 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -876,8 +876,36 @@ namespace MWWorld public: void load(ESM::ESMReader &esm, const std::string &id) { - mStatic.push_back(ESM::Pathgrid()); - mStatic.back().load(esm); + + ESM::Pathgrid pathgrid; + pathgrid.load(esm); + + // Try to overwrite existing record + // Can't use search() because we aren't sorted yet + if (!pathgrid.mCell.empty()) + { + for (std::vector::iterator it = mStatic.begin(); it != mStatic.end(); ++it) + { + if ((*it).mCell == pathgrid.mCell) + { + (*it) = pathgrid; + return; + } + } + } + else + { + for (std::vector::iterator it = mStatic.begin(); it != mStatic.end(); ++it) + { + if ((*it).mData.mX == pathgrid.mData.mX && (*it).mData.mY == pathgrid.mData.mY) + { + (*it) = pathgrid; + return; + } + } + } + + mStatic.push_back(pathgrid); } size_t getSize() const { From 7faa849cef8ac27f423f037ef5d0fcb89cefbd16 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 2 Dec 2014 01:19:35 +0100 Subject: [PATCH 171/303] Fix fatigue recalculation using older value (oops) --- apps/openmw/mwmechanics/creaturestats.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 7bf0277325..21fd2203fd 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -147,6 +147,11 @@ namespace MWMechanics if (value != currentValue) { + if(!mIsWerewolf) + mAttributes[index] = value; + else + mWerewolfAttributes[index] = value; + if (index == ESM::Attribute::Intelligence) mRecalcMagicka = true; else if (index == ESM::Attribute::Strength || @@ -164,11 +169,6 @@ namespace MWMechanics setFatigue(fatigue); } } - - if(!mIsWerewolf) - mAttributes[index] = value; - else - mWerewolfAttributes[index] = value; } void CreatureStats::setHealth(const DynamicStat &value) From 3519d23518f9647b1e972d8e73f703c87b1bcd95 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 2 Dec 2014 01:37:19 +0100 Subject: [PATCH 172/303] Race dialog: remove incorrect assumption about numeric index in head/hair record IDs --- apps/openmw/mwgui/race.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwgui/race.cpp b/apps/openmw/mwgui/race.cpp index 444b572b68..ec83b47a7d 100644 --- a/apps/openmw/mwgui/race.cpp +++ b/apps/openmw/mwgui/race.cpp @@ -128,11 +128,17 @@ namespace MWGui setRaceId(proto.mRace); recountParts(); - std::string index = proto.mHead.substr(proto.mHead.size() - 2, 2); - mFaceIndex = boost::lexical_cast(index) - 1; + for (unsigned int i=0; i(index) - 1; + for (unsigned int i=0; isetImageTexture (textureName); From e6c59f5585e73781c0c78c7d74f035fed8878e0a Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 2 Dec 2014 18:07:32 +0100 Subject: [PATCH 173/303] Revert "Allow NIF rotation matrices that include scale values" This reverts commit f57ddec6a22baf8823b93bd5e99df16796ac7d61. Conflicts: components/nif/nifstream.hpp (Fixes #2168) --- components/nif/data.hpp | 6 +++--- components/nif/nifstream.cpp | 2 +- components/nif/niftypes.hpp | 2 +- components/nif/node.cpp | 5 ++--- components/nifogre/mesh.cpp | 7 +++---- components/nifogre/skeleton.cpp | 14 +++----------- 6 files changed, 13 insertions(+), 23 deletions(-) diff --git a/components/nif/data.hpp b/components/nif/data.hpp index f3b5a27f8f..5efd0d6c99 100644 --- a/components/nif/data.hpp +++ b/components/nif/data.hpp @@ -272,7 +272,7 @@ class NiSkinData : public Record public: struct BoneTrafo { - Ogre::Matrix3 rotationScale; // Rotation offset from bone, non-uniform scale + Ogre::Matrix3 rotation; // Rotation offset from bone? Ogre::Vector3 trans; // Translation float scale; // Probably scale (always 1) }; @@ -295,7 +295,7 @@ public: void read(NIFStream *nif) { - trafo.rotationScale = nif->getMatrix3(); + trafo.rotation = nif->getMatrix3(); trafo.trans = nif->getVector3(); trafo.scale = nif->getFloat(); @@ -307,7 +307,7 @@ public: { BoneInfo &bi = bones[i]; - bi.trafo.rotationScale = nif->getMatrix3(); + bi.trafo.rotation = nif->getMatrix3(); bi.trafo.trans = nif->getVector3(); bi.trafo.scale = nif->getFloat(); bi.unknown = nif->getVector4(); diff --git a/components/nif/nifstream.cpp b/components/nif/nifstream.cpp index 527d1a2aff..a6fd5ef5ae 100644 --- a/components/nif/nifstream.cpp +++ b/components/nif/nifstream.cpp @@ -76,7 +76,7 @@ Transformation NIFStream::getTrafo() { Transformation t; t.pos = getVector3(); - t.rotationScale = getMatrix3(); + t.rotation = getMatrix3(); t.scale = getFloat(); return t; } diff --git a/components/nif/niftypes.hpp b/components/nif/niftypes.hpp index f9235ec45d..786c48b65e 100644 --- a/components/nif/niftypes.hpp +++ b/components/nif/niftypes.hpp @@ -35,7 +35,7 @@ namespace Nif struct Transformation { Ogre::Vector3 pos; - Ogre::Matrix3 rotationScale; + Ogre::Matrix3 rotation; float scale; static const Transformation& getIdentity() diff --git a/components/nif/node.cpp b/components/nif/node.cpp index 7529e602d9..b7ca981133 100644 --- a/components/nif/node.cpp +++ b/components/nif/node.cpp @@ -42,9 +42,8 @@ void Node::getProperties(const Nif::NiTexturingProperty *&texprop, Ogre::Matrix4 Node::getLocalTransform() const { - Ogre::Matrix4 mat4 = Ogre::Matrix4(trafo.rotationScale); - mat4.setTrans(trafo.pos); - mat4.setScale(Ogre::Vector3(trafo.rotationScale[0][0], trafo.rotationScale[1][1], trafo.rotationScale[2][2]) * trafo.scale); + Ogre::Matrix4 mat4 = Ogre::Matrix4(Ogre::Matrix4::IDENTITY); + mat4.makeTransform(trafo.pos, Ogre::Vector3(trafo.scale), Ogre::Quaternion(trafo.rotation)); return mat4; } diff --git a/components/nifogre/mesh.cpp b/components/nifogre/mesh.cpp index c952e664db..af73df637d 100644 --- a/components/nifogre/mesh.cpp +++ b/components/nifogre/mesh.cpp @@ -138,10 +138,9 @@ void NIFMeshLoader::createSubMesh(Ogre::Mesh *mesh, const Nif::NiTriShape *shape const Nif::NodeList &bones = skin->bones; for(size_t b = 0;b < bones.length();b++) { - const Ogre::Matrix3& rotationScale = data->bones[b].trafo.rotationScale; - Ogre::Matrix4 mat (rotationScale); - mat.setTrans(data->bones[b].trafo.trans); - mat.setScale(Ogre::Vector3(rotationScale[0][0], rotationScale[1][1], rotationScale[2][2]) * data->bones[b].trafo.scale); + Ogre::Matrix4 mat; + mat.makeTransform(data->bones[b].trafo.trans, Ogre::Vector3(data->bones[b].trafo.scale), + Ogre::Quaternion(data->bones[b].trafo.rotation)); mat = bones[b]->getWorldTransform() * mat; const std::vector &weights = data->bones[b].weights; diff --git a/components/nifogre/skeleton.cpp b/components/nifogre/skeleton.cpp index a3fade5b2c..db6a753c52 100644 --- a/components/nifogre/skeleton.cpp +++ b/components/nifogre/skeleton.cpp @@ -36,17 +36,9 @@ void NIFSkeletonLoader::buildBones(Ogre::Skeleton *skel, const Nif::Node *node, if(parent) parent->addChild(bone); mNifToOgreHandleMap[node->recIndex] = bone->getHandle(); - // decompose the local transform into position, scale and orientation. - // this is required for cases where the rotationScale matrix includes scaling, which the NIF format allows :( - // the code would look a bit nicer if Ogre allowed setting the transform matrix of a Bone directly, but we can't do that. - Ogre::Matrix4 mat(node->getLocalTransform()); - Ogre::Vector3 position, scale; - Ogre::Quaternion orientation; - mat.decomposition(position, scale, orientation); - bone->setOrientation(orientation); - bone->setPosition(position); - bone->setScale(scale); - + bone->setOrientation(node->trafo.rotation); + bone->setPosition(node->trafo.pos); + bone->setScale(Ogre::Vector3(node->trafo.scale)); bone->setBindingPose(); if(!(node->recType == Nif::RC_NiNode || /* Nothing special; children traversed below */ From 14ae6d28b09489ca7fc9599d5b27979f6c6fd8c1 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 2 Dec 2014 18:42:13 +0100 Subject: [PATCH 174/303] Fix being able to jump when overencumbered --- apps/openmw/mwclass/npc.cpp | 3 ++ apps/openmw/mwinput/inputmanagerimp.cpp | 1 + apps/openmw/mwmechanics/character.cpp | 45 +++++++++++++------------ 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index d3f86c03b0..641edcc834 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -969,6 +969,9 @@ namespace MWClass float Npc::getJump(const MWWorld::Ptr &ptr) const { + if(getEncumbrance(ptr) > getCapacity(ptr)) + return 0.f; + const NpcCustomData *npcdata = static_cast(ptr.getRefData().getCustomData()); const GMST& gmst = getGmst(); const MWMechanics::MagicEffects &mageffects = npcdata->mNpcStats.getMagicEffects(); diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 48acd22ba3..b0d2bfefff 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -371,6 +371,7 @@ namespace MWInput { mPlayer->setUpDown (1); triedToMove = true; + mOverencumberedMessageDelay = 0.f; } if (mAlwaysRunActive) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 11484ac49a..2e4f76c1a1 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1414,29 +1414,32 @@ void CharacterController::update(float duration) { // Started a jump. float z = cls.getJump(mPtr); - if(vec.x == 0 && vec.y == 0) - vec = Ogre::Vector3(0.0f, 0.0f, z); - else + if (z > 0) { - Ogre::Vector3 lat = Ogre::Vector3(vec.x, vec.y, 0.0f).normalisedCopy(); - vec = Ogre::Vector3(lat.x, lat.y, 1.0f) * z * 0.707f; + if(vec.x == 0 && vec.y == 0) + vec = Ogre::Vector3(0.0f, 0.0f, z); + else + { + Ogre::Vector3 lat = Ogre::Vector3(vec.x, vec.y, 0.0f).normalisedCopy(); + vec = Ogre::Vector3(lat.x, lat.y, 1.0f) * z * 0.707f; + } + + // advance acrobatics + if (mPtr.getRefData().getHandle() == "player") + cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, 0); + + // decrease fatigue + const MWWorld::Store &gmst = world->getStore().get(); + const float fatigueJumpBase = gmst.find("fFatigueJumpBase")->getFloat(); + const float fatigueJumpMult = gmst.find("fFatigueJumpMult")->getFloat(); + float normalizedEncumbrance = mPtr.getClass().getNormalizedEncumbrance(mPtr); + if (normalizedEncumbrance > 1) + normalizedEncumbrance = 1; + const int fatigueDecrease = fatigueJumpBase + (1 - normalizedEncumbrance) * fatigueJumpMult; + DynamicStat fatigue = cls.getCreatureStats(mPtr).getFatigue(); + fatigue.setCurrent(fatigue.getCurrent() - fatigueDecrease); + cls.getCreatureStats(mPtr).setFatigue(fatigue); } - - // advance acrobatics - if (mPtr.getRefData().getHandle() == "player") - cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, 0); - - // decrease fatigue - const MWWorld::Store &gmst = world->getStore().get(); - const float fatigueJumpBase = gmst.find("fFatigueJumpBase")->getFloat(); - const float fatigueJumpMult = gmst.find("fFatigueJumpMult")->getFloat(); - float normalizedEncumbrance = mPtr.getClass().getNormalizedEncumbrance(mPtr); - if (normalizedEncumbrance > 1) - normalizedEncumbrance = 1; - const int fatigueDecrease = fatigueJumpBase + (1 - normalizedEncumbrance) * fatigueJumpMult; - DynamicStat fatigue = cls.getCreatureStats(mPtr).getFatigue(); - fatigue.setCurrent(fatigue.getCurrent() - fatigueDecrease); - cls.getCreatureStats(mPtr).setFatigue(fatigue); } else if(mJumpState == JumpState_InAir) { From b650338d6934be67ccdebdf9c6350e8b39b050d7 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 2 Dec 2014 18:42:40 +0100 Subject: [PATCH 175/303] Implement drawMode of NiStencilProperty (Feature #1057) --- components/nif/node.cpp | 7 +++++-- components/nif/node.hpp | 3 ++- components/nifogre/material.cpp | 28 ++++++++++++++++++++++++++++ components/nifogre/material.hpp | 2 ++ components/nifogre/mesh.cpp | 5 +++-- components/nifogre/ogrenifloader.cpp | 8 +++++--- files/materials/objects.mat | 1 + 7 files changed, 46 insertions(+), 8 deletions(-) diff --git a/components/nif/node.cpp b/components/nif/node.cpp index b7ca981133..fb68da548d 100644 --- a/components/nif/node.cpp +++ b/components/nif/node.cpp @@ -9,10 +9,11 @@ void Node::getProperties(const Nif::NiTexturingProperty *&texprop, const Nif::NiVertexColorProperty *&vertprop, const Nif::NiZBufferProperty *&zprop, const Nif::NiSpecularProperty *&specprop, - const Nif::NiWireframeProperty *&wireprop) const + const Nif::NiWireframeProperty *&wireprop, + const Nif::NiStencilProperty *&stencilprop) const { if(parent) - parent->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop); + parent->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop, stencilprop); for(size_t i = 0;i < props.length();i++) { @@ -35,6 +36,8 @@ void Node::getProperties(const Nif::NiTexturingProperty *&texprop, specprop = static_cast(pr); else if(pr->recType == Nif::RC_NiWireframeProperty) wireprop = static_cast(pr); + else if (pr->recType == Nif::RC_NiStencilProperty) + stencilprop = static_cast(pr); else std::cerr<< "Unhandled property type: "<recName <colors.size() != 0); @@ -205,6 +207,20 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, } } + if(stencilprop) + { + drawMode = stencilprop->data.drawMode; + if (stencilprop->data.enabled) + warn("Unhandled stencil test in "+name); + + Nif::ControllerPtr ctrls = stencilprop->controller; + while(!ctrls.empty()) + { + warn("Unhandled stencil controller "+ctrls->recName+" in "+name); + ctrls = ctrls->next; + } + } + // Material if(matprop) { @@ -249,8 +265,13 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, for(int i = 0;i < 7;i++) { if(!texName[i].empty()) + { boost::hash_combine(h, texName[i]); + boost::hash_combine(h, texprop->textures[i].clamp); + boost::hash_combine(h, texprop->textures[i].uvSet); + } } + boost::hash_combine(h, drawMode); boost::hash_combine(h, vertexColour); boost::hash_combine(h, alphaFlags); boost::hash_combine(h, alphaTest); @@ -308,6 +329,13 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, instance->setProperty("polygon_mode", sh::makeProperty(new sh::StringValue("wireframe"))); } + if (drawMode == 1) + instance->setProperty("cullmode", sh::makeProperty(new sh::StringValue("clockwise"))); + else if (drawMode == 2) + instance->setProperty("cullmode", sh::makeProperty(new sh::StringValue("anticlockwise"))); + else if (drawMode == 3) + instance->setProperty("cullmode", sh::makeProperty(new sh::StringValue("none"))); + instance->setProperty("diffuseMap", sh::makeProperty(texName[Nif::NiTexturingProperty::BaseTexture])); instance->setProperty("normalMap", sh::makeProperty(texName[Nif::NiTexturingProperty::BumpTexture])); instance->setProperty("detailMap", sh::makeProperty(texName[Nif::NiTexturingProperty::DetailTexture])); diff --git a/components/nifogre/material.hpp b/components/nifogre/material.hpp index d485439cf2..6be52d1a56 100644 --- a/components/nifogre/material.hpp +++ b/components/nifogre/material.hpp @@ -18,6 +18,7 @@ namespace Nif class NiZBufferProperty; class NiSpecularProperty; class NiWireframeProperty; + class NiStencilProperty; } namespace NifOgre @@ -41,6 +42,7 @@ public: const Nif::NiZBufferProperty *zprop, const Nif::NiSpecularProperty *specprop, const Nif::NiWireframeProperty *wireprop, + const Nif::NiStencilProperty *stencilprop, bool &needTangents, bool particleMaterial=false); }; diff --git a/components/nifogre/mesh.cpp b/components/nifogre/mesh.cpp index af73df637d..4932dd0098 100644 --- a/components/nifogre/mesh.cpp +++ b/components/nifogre/mesh.cpp @@ -320,13 +320,14 @@ void NIFMeshLoader::createSubMesh(Ogre::Mesh *mesh, const Nif::NiTriShape *shape const Nif::NiZBufferProperty *zprop = NULL; const Nif::NiSpecularProperty *specprop = NULL; const Nif::NiWireframeProperty *wireprop = NULL; + const Nif::NiStencilProperty *stencilprop = NULL; bool needTangents = false; - shape->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop); + shape->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop, stencilprop); std::string matname = NIFMaterialLoader::getMaterial(data, mesh->getName(), mGroup, texprop, matprop, alphaprop, vertprop, zprop, specprop, - wireprop, needTangents); + wireprop, stencilprop, needTangents); if(matname.length() > 0) sub->setMaterialName(matname); diff --git a/components/nifogre/ogrenifloader.cpp b/components/nifogre/ogrenifloader.cpp index dcc34f6273..053adfb5b8 100644 --- a/components/nifogre/ogrenifloader.cpp +++ b/components/nifogre/ogrenifloader.cpp @@ -732,7 +732,8 @@ class NIFObjectLoader const Nif::NiZBufferProperty *zprop = NULL; const Nif::NiSpecularProperty *specprop = NULL; const Nif::NiWireframeProperty *wireprop = NULL; - node->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop); + const Nif::NiStencilProperty *stencilprop = NULL; + node->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop, stencilprop); Ogre::ControllerValueRealPtr srcval((animflags&Nif::NiNode::AnimFlag_AutoPlay) ? Ogre::ControllerManager::getSingleton().getFrameTimeSource() : @@ -889,13 +890,14 @@ class NIFObjectLoader const Nif::NiZBufferProperty *zprop = NULL; const Nif::NiSpecularProperty *specprop = NULL; const Nif::NiWireframeProperty *wireprop = NULL; + const Nif::NiStencilProperty *stencilprop = NULL; bool needTangents = false; - partnode->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop); + partnode->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop, stencilprop); partsys->setMaterialName(NIFMaterialLoader::getMaterial(particledata, fullname, group, texprop, matprop, alphaprop, vertprop, zprop, specprop, - wireprop, needTangents, + wireprop, stencilprop, needTangents, // MW doesn't light particles, but the MaterialProperty // used still has lighting, so that must be ignored. true)); diff --git a/files/materials/objects.mat b/files/materials/objects.mat index 149160760b..7d3085b0f2 100644 --- a/files/materials/objects.mat +++ b/files/materials/objects.mat @@ -67,6 +67,7 @@ material openmw_objects_base depth_check $depth_check transparent_sorting $transparent_sorting polygon_mode $polygon_mode + cull_hardware $cullmode texture_unit diffuseMap { From 75b0da5dce41960b3e9af9fdb69db1d9b3e20fdc Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 2 Dec 2014 21:04:38 +0100 Subject: [PATCH 176/303] Don't updateBoneTree for non-skinned parts (Fixes #2124) --- apps/openmw/mwrender/npcanimation.cpp | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index c43d3663eb..363a363763 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -60,6 +60,21 @@ std::string getVampireHead(const std::string& race, bool female) return "meshes\\" + sVampireMapping[thisCombination]->mModel; } +bool isSkinned (NifOgre::ObjectScenePtr scene) +{ + if (scene->mSkelBase == NULL) + return false; + for(size_t j = 0; j < scene->mEntities.size(); j++) + { + Ogre::Entity *ent = scene->mEntities[j]; + if(scene->mSkelBase != ent && ent->hasSkeleton()) + { + return true; + } + } + return false; +} + } @@ -611,10 +626,11 @@ Ogre::Vector3 NpcAnimation::runAnimation(float timepassed) for(;ctrl != mObjectParts[i]->mControllers.end();++ctrl) ctrl->update(); - Ogre::Entity *ent = mObjectParts[i]->mSkelBase; - if(!ent) continue; - updateSkeletonInstance(baseinst, ent->getSkeleton()); - ent->getAllAnimationStates()->_notifyDirty(); + if (!isSkinned(mObjectParts[i])) + continue; + + updateSkeletonInstance(baseinst, mObjectParts[i]->mSkelBase->getSkeleton()); + mObjectParts[i]->mSkelBase->getAllAnimationStates()->_notifyDirty(); } return ret; @@ -697,7 +713,8 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g } } - updateSkeletonInstance(mSkelBase->getSkeleton(), skel); + if (isSkinned(mObjectParts[type])) + updateSkeletonInstance(mSkelBase->getSkeleton(), skel); } std::vector >::iterator ctrl(mObjectParts[type]->mControllers.begin()); From fee08f97edf41eab9f28e538ae9bca4205b0877f Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 3 Dec 2014 00:02:14 +0100 Subject: [PATCH 177/303] Fix crash in character preview for non-existing meshes (Fixes #2128) --- apps/openmw/mwgui/race.cpp | 10 +++++++++- apps/openmw/mwrender/characterpreview.cpp | 5 ++++- apps/openmw/mwrender/npcanimation.cpp | 10 +++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwgui/race.cpp b/apps/openmw/mwgui/race.cpp index ec83b47a7d..bcb766f8f3 100644 --- a/apps/openmw/mwgui/race.cpp +++ b/apps/openmw/mwgui/race.cpp @@ -313,7 +313,15 @@ namespace MWGui record.mHead = mAvailableHeads[mFaceIndex]; record.mHair = mAvailableHairs[mHairIndex]; - mPreview->setPrototype(record); + try + { + mPreview->setPrototype(record); + } + catch (std::exception& e) + { + std::cerr << "Error creating preview: " << e.what() << std::endl; + } + mPreviewDirty = true; } diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index d9c953133e..92d0bcd557 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -115,8 +115,8 @@ namespace MWRender void CharacterPreview::rebuild() { - assert(mAnimation); delete mAnimation; + mAnimation = NULL; mAnimation = new NpcAnimation(mCharacter, mNode, 0, true, true, (renderHeadOnly() ? NpcAnimation::VM_HeadOnly : NpcAnimation::VM_Normal)); @@ -187,6 +187,9 @@ namespace MWRender void InventoryPreview::update() { + if (!mAnimation) + return; + mAnimation->updateParts(); MWWorld::InventoryStore &inv = mCharacter.getClass().getInventoryStore(mCharacter); diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 363a363763..d20e89324d 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -676,7 +676,15 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g removeIndividualPart(type); mPartslots[type] = group; mPartPriorities[type] = priority; - mObjectParts[type] = insertBoundedPart(mesh, group, sPartList.at(type), enchantedGlow, glowColor); + try + { + mObjectParts[type] = insertBoundedPart(mesh, group, sPartList.at(type), enchantedGlow, glowColor); + } + catch (std::exception& e) + { + std::cerr << "Error adding NPC part: " << e.what() << std::endl; + return false; + } if (!mSoundsDisabled) { From d67ad9037e6586306b5d82d792e8924c89e4280b Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 3 Dec 2014 20:55:33 +0100 Subject: [PATCH 178/303] increased version number --- CMakeLists.txt | 4 ++-- readme.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a923c6312..9587c652c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,8 +12,8 @@ set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/) message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) -set(OPENMW_VERSION_MINOR 33) -set(OPENMW_VERSION_RELEASE 1) +set(OPENMW_VERSION_MINOR 34) +set(OPENMW_VERSION_RELEASE 0) set(OPENMW_VERSION_COMMITHASH "") set(OPENMW_VERSION_TAGHASH "") diff --git a/readme.txt b/readme.txt index 810a0e055e..f4493c80e7 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ OpenMW: A reimplementation of The Elder Scrolls III: Morrowind OpenMW is an attempt at recreating the engine for the popular role-playing game Morrowind by Bethesda Softworks. You need to own and install the original game for OpenMW to work. -Version: 0.33.1 +Version: 0.34.0 License: GPL (see GPL3.txt for more information) Website: http://www.openmw.org From 41ec6e42cbbebc7cd11c3bf41328397b8497d3e6 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 3 Dec 2014 20:58:44 +0100 Subject: [PATCH 179/303] updated changelog --- readme.txt | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/readme.txt b/readme.txt index f4493c80e7..91ec7962f1 100644 --- a/readme.txt +++ b/readme.txt @@ -98,6 +98,82 @@ Allowed options: CHANGELOG +0.34.0 + +Bug #904: omwlauncher doesn't allow installing Tribunal and Bloodmoon if only MW is installed +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 #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 #2173: Launcher: disabling plugin files is broken +Bug #2175: Pathgrid mods do not overwrite the existing pathgrid +Bug #2180: Editor: Verifier doesn't handle Windows-specific path issues when dealing with resources +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 From e67cf96250db8d1d11dc860d664a4b0682e8f39e Mon Sep 17 00:00:00 2001 From: cc9cii Date: Fri, 5 Dec 2014 01:09:42 +1100 Subject: [PATCH 180/303] Allow only one instance of OpenCS. Only tested on windows x64. --- apps/opencs/editor.cpp | 57 +++++++++++++++++++++++++++++++++++++++--- apps/opencs/editor.hpp | 4 +++ apps/opencs/main.cpp | 2 +- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index bef83b8ac7..dc4044fea9 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -22,7 +22,7 @@ CS::Editor::Editor (OgreInit::OgreInit& ogreInit) : mUserSettings (mCfgMgr), mOverlaySystem (0), mDocumentManager (mCfgMgr), mViewManager (mDocumentManager), mPhysicsManager (0), - mIpcServerName ("org.openmw.OpenCS"), mServer(NULL), mClientSocket(NULL) + mIpcServerName ("org.openmw.OpenCS"), mServer(NULL), mClientSocket(NULL), mPid(""), mLock() { std::pair > config = readConfig(); @@ -70,7 +70,10 @@ CS::Editor::Editor (OgreInit::OgreInit& ogreInit) } CS::Editor::~Editor () -{} +{ + if(mServer && boost::filesystem::exists(mPid)) + remove(mPid.string().c_str()); // ignore error +} void CS::Editor::setupDataFiles (const Files::PathContainer& dataDirs) { @@ -233,7 +236,54 @@ void CS::Editor::showSettings() bool CS::Editor::makeIPCServer() { - mServer = new QLocalServer(this); + try + { + mPid = boost::filesystem::temp_directory_path(); + mPid += "opencs.pid"; + bool pidExists = boost::filesystem::exists(mPid); + + boost::filesystem::ofstream tempFile(mPid); + + mLock = boost::interprocess::file_lock(mPid.string().c_str()); + if(!mLock.try_lock()) + { + std::cerr << "OpenCS already running." << std::endl; + return false; + } + +#ifdef _WIN32 + tempFile << GetCurrentProcessId() << std::endl; +#else + tempFile << getpid() << std::endl; +#endif + tempFile.close(); + + 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.toStdString().c_str())) + { + // TODO: compare pid of the current process with that in the file + std::cout << "Detected unclean shutdown." << std::endl; + // delete the stale file + if(remove(fullPath.toStdString().c_str())) + std::cerr << "ERROR removing stale connection file" << std::endl; + } + } + } + + catch(const std::exception& e) + { + std::cerr << "ERROR " << e.what() << std::endl; + return false; + } if(mServer->listen(mIpcServerName)) { @@ -242,6 +292,7 @@ bool CS::Editor::makeIPCServer() } mServer->close(); + mServer = NULL; return false; } diff --git a/apps/opencs/editor.hpp b/apps/opencs/editor.hpp index d55b0e873e..c8a6c43c3f 100644 --- a/apps/opencs/editor.hpp +++ b/apps/opencs/editor.hpp @@ -3,6 +3,8 @@ #include +#include + #include #include #include @@ -54,6 +56,8 @@ namespace CS CSVDoc::FileDialog mFileDialog; boost::filesystem::path mLocal; boost::filesystem::path mResources; + boost::filesystem::path mPid; + boost::interprocess::file_lock mLock; bool mFsStrict; void setupDataFiles (const Files::PathContainer& dataDirs); diff --git a/apps/opencs/main.cpp b/apps/opencs/main.cpp index b184a1ef1e..dd20324d12 100644 --- a/apps/opencs/main.cpp +++ b/apps/opencs/main.cpp @@ -83,7 +83,7 @@ int main(int argc, char *argv[]) if(!editor.makeIPCServer()) { editor.connectToIPCServer(); - // return 0; + return 0; } shinyFactory = editor.setupGraphics(); From 07f10a014071bbe642ac463683c79daf8e964762 Mon Sep 17 00:00:00 2001 From: cc9cii Date: Fri, 5 Dec 2014 01:32:20 +1100 Subject: [PATCH 181/303] Use append syntax compatible with older versions of boost. --- apps/opencs/editor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index dc4044fea9..2aee21d382 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -239,7 +239,7 @@ bool CS::Editor::makeIPCServer() try { mPid = boost::filesystem::temp_directory_path(); - mPid += "opencs.pid"; + mPid /= "opencs.pid"; bool pidExists = boost::filesystem::exists(mPid); boost::filesystem::ofstream tempFile(mPid); From 6731afc79c44a591887b0ef91d9c2c6c2468162c Mon Sep 17 00:00:00 2001 From: cc9cii Date: Fri, 5 Dec 2014 03:59:16 +1100 Subject: [PATCH 182/303] Use float for setting skill use values. Should resolve bug #2183. --- apps/opencs/model/world/columnimp.hpp | 2 +- apps/opencs/view/world/util.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/opencs/model/world/columnimp.hpp b/apps/opencs/model/world/columnimp.hpp index 6f830e6e3e..eba73cf0b0 100644 --- a/apps/opencs/model/world/columnimp.hpp +++ b/apps/opencs/model/world/columnimp.hpp @@ -272,7 +272,7 @@ namespace CSMWorld { ESXRecordT record2 = record.get(); - record2.mData.mUseValue[mIndex] = data.toInt(); + record2.mData.mUseValue[mIndex] = data.toFloat(); record.setModified (record2); } diff --git a/apps/opencs/view/world/util.cpp b/apps/opencs/view/world/util.cpp index f987c8d9f2..c65e12c609 100644 --- a/apps/opencs/view/world/util.cpp +++ b/apps/opencs/view/world/util.cpp @@ -173,6 +173,8 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO { QDoubleSpinBox *dsb = new QDoubleSpinBox(parent); dsb->setRange(FLT_MIN, FLT_MAX); + dsb->setSingleStep(0.01f); + dsb->setDecimals(3); return dsb; } From ab693f1f649c497f9d0b00e2491ed23eb26a7c86 Mon Sep 17 00:00:00 2001 From: cc9cii Date: Fri, 5 Dec 2014 07:50:03 +1100 Subject: [PATCH 183/303] Workaround file lock being lost if the same file is closed elsewhere in the program (see https://svn.boost.org/trac/boost/ticket/3582) --- apps/opencs/editor.cpp | 11 ++++++----- apps/opencs/editor.hpp | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 9fe974d86b..f609b80b74 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -72,8 +72,10 @@ CS::Editor::Editor (OgreInit::OgreInit& ogreInit) CS::Editor::~Editor () { + mPidFile.close(); + if(mServer && boost::filesystem::exists(mPid)) - remove(mPid.string().c_str()); // ignore error + remove(mPid.string().c_str()); // ignore any error // cleanup global resources used by OEngine delete OEngine::Physic::BulletShapeManager::getSingletonPtr(); @@ -246,7 +248,7 @@ bool CS::Editor::makeIPCServer() mPid /= "opencs.pid"; bool pidExists = boost::filesystem::exists(mPid); - boost::filesystem::ofstream tempFile(mPid); + mPidFile.open(mPid); mLock = boost::interprocess::file_lock(mPid.string().c_str()); if(!mLock.try_lock()) @@ -256,11 +258,10 @@ bool CS::Editor::makeIPCServer() } #ifdef _WIN32 - tempFile << GetCurrentProcessId() << std::endl; + mPidFile << GetCurrentProcessId() << std::endl; #else - tempFile << getpid() << std::endl; + mPidFile << getpid() << std::endl; #endif - tempFile.close(); mServer = new QLocalServer(this); diff --git a/apps/opencs/editor.hpp b/apps/opencs/editor.hpp index 30a031309b..273f0825b8 100644 --- a/apps/opencs/editor.hpp +++ b/apps/opencs/editor.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -56,6 +57,7 @@ namespace CS boost::filesystem::path mResources; boost::filesystem::path mPid; boost::interprocess::file_lock mLock; + boost::filesystem::ofstream mPidFile; bool mFsStrict; void setupDataFiles (const Files::PathContainer& dataDirs); From 83dcf9ce4bb7fd2d78c5e1ef0395eceb58505f8d Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 5 Dec 2014 02:17:15 +0100 Subject: [PATCH 184/303] Overwrite existing records in IndexedStore (Fixes #2182) --- apps/openmw/mwgui/tooltips.cpp | 4 +- .../mwmechanics/mechanicsmanagerimp.cpp | 4 +- apps/openmw/mwworld/store.hpp | 66 ++++--------------- 3 files changed, 18 insertions(+), 56 deletions(-) diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index 4608010ac3..396c8fa489 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -633,8 +633,8 @@ namespace MWGui MWWorld::Store::iterator it = skills.begin(); for (; it != skills.end(); ++it) { - if (it->mData.mSpecialization == specId) - specText += std::string("\n#{") + ESM::Skill::sSkillNameIds[it->mIndex] + "}"; + if (it->second.mData.mSpecialization == specId) + specText += std::string("\n#{") + ESM::Skill::sSkillNameIds[it->first] + "}"; } widget->setUserString("Caption_CenteredCaptionText", specText); widget->setUserString("ToolTipLayout", "TextWithCenteredCaptionToolTip"); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index b4edf44aa9..9cafe9b3ce 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -143,9 +143,9 @@ namespace MWMechanics MWWorld::Store::iterator iter = skills.begin(); for (; iter != skills.end(); ++iter) { - if (iter->mData.mSpecialization==class_->mData.mSpecialization) + if (iter->second.mData.mSpecialization==class_->mData.mSpecialization) { - int index = iter->mIndex; + int index = iter->first; if (index>=0 && index<27) { diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index 469b93f88e..907d9bd430 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -1015,24 +1015,15 @@ namespace MWWorld template class IndexedStore { - struct Compare - { - bool operator()(const T &x, const T &y) const { - return x.mIndex < y.mIndex; - } - }; protected: - std::vector mStatic; + typedef typename std::map Static; + Static mStatic; public: - typedef typename std::vector::const_iterator iterator; + typedef typename std::map::const_iterator iterator; IndexedStore() {} - IndexedStore(unsigned int size) { - mStatic.reserve(size); - } - iterator begin() const { return mStatic.begin(); } @@ -1041,10 +1032,14 @@ namespace MWWorld return mStatic.end(); } - /// \todo refine loading order void load(ESM::ESMReader &esm) { - mStatic.push_back(T()); - mStatic.back().load(esm); + T record; + record.load(esm); + + // Try to overwrite existing record + std::pair found = mStatic.insert(std::make_pair(record.mIndex, record)); + if (found.second) + found.first->second = record; } int getSize() const { @@ -1052,40 +1047,13 @@ namespace MWWorld } void setUp() { - /// \note This method sorts indexed values for further - /// searches. Every loaded item is present in storage, but - /// latest loaded shadows any previous while searching. - /// If memory cost will be too high, it is possible to remove - /// unused values. - - Compare cmp; - - std::stable_sort(mStatic.begin(), mStatic.end(), cmp); - - typename std::vector::iterator first, next; - next = first = mStatic.begin(); - - while (first != mStatic.end() && ++next != mStatic.end()) { - while (next != mStatic.end() && !cmp(*first, *next)) { - ++next; - } - if (first != --next) { - std::swap(*first, *next); - } - first = ++next; - } } const T *search(int index) const { - T item; - item.mIndex = index; - - iterator it = - std::lower_bound(mStatic.begin(), mStatic.end(), item, Compare()); - if (it != mStatic.end() && it->mIndex == index) { - return &(*it); - } - return 0; + typename Static::const_iterator it = mStatic.find(index); + if (it != mStatic.end()) + return &(it->second); + return NULL; } const T *find(int index) const { @@ -1103,18 +1071,12 @@ namespace MWWorld struct Store : public IndexedStore { Store() {} - Store(unsigned int size) - : IndexedStore(size) - {} }; template <> struct Store : public IndexedStore { Store() {} - Store(unsigned int size) - : IndexedStore(size) - {} }; template <> From a67e7c64eadbfe23aa20214f44a5a68c95a40e35 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 5 Dec 2014 15:58:22 +0100 Subject: [PATCH 185/303] Optimize pathgrid store --- apps/openmw/mwworld/store.hpp | 124 ++++++---------------------------- 1 file changed, 20 insertions(+), 104 deletions(-) diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index 907d9bd430..5966a99b9e 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -842,36 +842,12 @@ namespace MWWorld template <> class Store : public StoreBase { - public: - typedef std::vector::const_iterator iterator; - private: - std::vector mStatic; + typedef std::map Interior; + typedef std::map, ESM::Pathgrid> Exterior; - std::vector::iterator mIntBegin, mIntEnd, mExtBegin, mExtEnd; - - struct IntExtOrdering - { - bool operator()(const ESM::Pathgrid &x, const ESM::Pathgrid &y) const { - // interior pathgrids precedes exterior ones (x < y) - if ((x.mData.mX == 0 && x.mData.mY == 0) && - (y.mData.mX != 0 || y.mData.mY != 0)) - { - return true; - } - return false; - } - }; - - struct ExtCompare - { - bool operator()(const ESM::Pathgrid &x, const ESM::Pathgrid &y) const { - if (x.mData.mX == y.mData.mX) { - return x.mData.mY < y.mData.mY; - } - return x.mData.mX < y.mData.mX; - } - }; + Interior mInt; + Exterior mExt; public: @@ -881,65 +857,33 @@ namespace MWWorld pathgrid.load(esm); // Try to overwrite existing record - // Can't use search() because we aren't sorted yet if (!pathgrid.mCell.empty()) { - for (std::vector::iterator it = mStatic.begin(); it != mStatic.end(); ++it) - { - if ((*it).mCell == pathgrid.mCell) - { - (*it) = pathgrid; - return; - } - } + std::pair found = mInt.insert(std::make_pair(pathgrid.mCell, pathgrid)); + if (found.second) + found.first->second = pathgrid; } else { - for (std::vector::iterator it = mStatic.begin(); it != mStatic.end(); ++it) - { - if ((*it).mData.mX == pathgrid.mData.mX && (*it).mData.mY == pathgrid.mData.mY) - { - (*it) = pathgrid; - return; - } - } + std::pair found = mExt.insert(std::make_pair(std::make_pair(pathgrid.mData.mX, pathgrid.mData.mY), + pathgrid)); + if (found.second) + found.first->second = pathgrid; } - - mStatic.push_back(pathgrid); } size_t getSize() const { - return mStatic.size(); + return mInt.size() + mExt.size(); } void setUp() { - IntExtOrdering cmp; - std::sort(mStatic.begin(), mStatic.end(), cmp); - - ESM::Pathgrid pg; - pg.mData.mX = pg.mData.mY = 1; - mExtBegin = - std::lower_bound(mStatic.begin(), mStatic.end(), pg, cmp); - mExtEnd = mStatic.end(); - - mIntBegin = mStatic.begin(); - mIntEnd = mExtBegin; - - std::sort(mIntBegin, mIntEnd, RecordCmp()); - std::sort(mExtBegin, mExtEnd, ExtCompare()); } const ESM::Pathgrid *search(int x, int y) const { - ESM::Pathgrid pg; - pg.mData.mX = x; - pg.mData.mY = y; - - iterator it = - std::lower_bound(mExtBegin, mExtEnd, pg, ExtCompare()); - if (it != mExtEnd && it->mData.mX == x && it->mData.mY == y) { - return &(*it); - } - return 0; + Exterior::const_iterator it = mExt.find(std::make_pair(x,y)); + if (it != mExt.end()) + return &(it->second); + return NULL; } const ESM::Pathgrid *find(int x, int y) const { @@ -953,14 +897,10 @@ namespace MWWorld } const ESM::Pathgrid *search(const std::string &name) const { - ESM::Pathgrid pg; - pg.mCell = name; - - iterator it = std::lower_bound(mIntBegin, mIntEnd, pg, RecordCmp()); - if (it != mIntEnd && Misc::StringUtils::ciEqual(it->mCell, name)) { - return &(*it); - } - return 0; + Interior::const_iterator it = mInt.find(name); + if (it != mInt.end()) + return &(it->second); + return NULL; } const ESM::Pathgrid *find(const std::string &name) const { @@ -986,30 +926,6 @@ namespace MWWorld } return find(cell.mData.mX, cell.mData.mY); } - - iterator begin() const { - return mStatic.begin(); - } - - iterator end() const { - return mStatic.end(); - } - - iterator interiorPathsBegin() const { - return mIntBegin; - } - - iterator interiorPathsEnd() const { - return mIntEnd; - } - - iterator exteriorPathsBegin() const { - return mExtBegin; - } - - iterator exteriorPathsEnd() const { - return mExtEnd; - } }; template From 65536f085768fd1d966a603a7e7f4d48f407da8b Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 5 Dec 2014 18:00:30 +0100 Subject: [PATCH 186/303] Load initial particle system state from NIF files (Fixes #2178) --- apps/openmw/mwrender/animation.cpp | 4 -- apps/openmw/mwrender/npcanimation.cpp | 4 -- components/nifogre/ogrenifloader.cpp | 82 +++++++++++++++++++++++++++ components/nifogre/ogrenifloader.hpp | 4 ++ 4 files changed, 86 insertions(+), 8 deletions(-) diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 548906cdf2..ecaaba0b9d 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -114,10 +114,6 @@ void Animation::setObjectRoot(const std::string &model, bool baseonly) mObjectRoot = (!baseonly ? NifOgre::Loader::createObjects(mInsert, mdlname) : NifOgre::Loader::createObjectBase(mInsert, mdlname)); - // Fast forward auto-play particles, which will have been set up as Emitting by the loader. - for (unsigned int i=0; imParticles.size(); ++i) - mObjectRoot->mParticles[i]->fastForward(1, 0.1); - if(mObjectRoot->mSkelBase) { mSkelBase = mObjectRoot->mSkelBase; diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index d20e89324d..f2bc3df95f 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -571,10 +571,6 @@ NifOgre::ObjectScenePtr NpcAnimation::insertBoundedPart(const std::string &model std::for_each(objects->mEntities.begin(), objects->mEntities.end(), SetObjectGroup(group)); std::for_each(objects->mParticles.begin(), objects->mParticles.end(), SetObjectGroup(group)); - // Fast forward auto-play particles, which will have been set up as Emitting by the loader. - for (unsigned int i=0; imParticles.size(); ++i) - objects->mParticles[i]->fastForward(1, 0.1); - if(objects->mSkelBase) { Ogre::AnimationStateSet *aset = objects->mSkelBase->getAllAnimationStates(); diff --git a/components/nifogre/ogrenifloader.cpp b/components/nifogre/ogrenifloader.cpp index 053adfb5b8..b18a88d140 100644 --- a/components/nifogre/ogrenifloader.cpp +++ b/components/nifogre/ogrenifloader.cpp @@ -161,6 +161,38 @@ void ObjectScene::rotateBillboardNodes(Ogre::Camera *camera) } } +void ObjectScene::_notifyAttached() +{ + // convert initial particle positions to world space for world-space particle systems + // this can't be done on creation because the particle system is not in its correct world space position yet + for (std::vector::iterator it = mParticles.begin(); it != mParticles.end(); ++it) + { + Ogre::ParticleSystem* psys = *it; + if (psys->getKeepParticlesInLocalSpace()) + continue; + Ogre::ParticleIterator pi = psys->_getIterator(); + while (!pi.end()) + { + Ogre::Particle *p = pi.getNext(); + +#if OGRE_VERSION >= (1 << 16 | 10 << 8 | 0) + Ogre::Vector3& position = p->mPosition; + Ogre::Vector3& direction = p->mDirection; +#else + Ogre::Vector3& position = p->position; + Ogre::Vector3& direction = p->direction; +#endif + + position = + (psys->getParentNode()->_getDerivedOrientation() * + (psys->getParentNode()->_getDerivedScale() * position)) + + psys->getParentNode()->_getDerivedPosition(); + direction = + (psys->getParentNode()->_getDerivedOrientation() * direction); + } + } +} + // Animates a texture class FlipController { @@ -949,6 +981,8 @@ class NIFObjectLoader createParticleEmitterAffectors(partsys, partctrl, trgtbone, scene->mSkelBase->getName()); } + createParticleInitialState(partsys, particledata, partctrl); + Ogre::ControllerValueRealPtr srcval((partflags&Nif::NiNode::ParticleFlag_AutoPlay) ? Ogre::ControllerManager::getSingleton().getFrameTimeSource() : Ogre::ControllerValueRealPtr()); @@ -975,6 +1009,50 @@ class NIFObjectLoader createMaterialControllers(partnode, partsys, animflags, scene); } + static void createParticleInitialState(Ogre::ParticleSystem* partsys, const Nif::NiAutoNormalParticlesData* particledata, + const Nif::NiParticleSystemController* partctrl) + { + partsys->_update(0.f); // seems to be required to allocate mFreeParticles + int i=0; + for (std::vector::const_iterator it = partctrl->particles.begin(); + iactiveCount && it != partctrl->particles.end(); ++it, ++i) + { + const Nif::NiParticleSystemController::Particle& particle = *it; + + Ogre::Particle* created = partsys->createParticle(); + if (!created) + break; + +#if OGRE_VERSION >= (1 << 16 | 10 << 8 | 0) + Ogre::Vector3& position = created->mPosition; + Ogre::Vector3& direction = created->mDirection; + Ogre::ColourValue& colour = created->mColour; + float& totalTimeToLive = created->mTotalTimeToLive; + float& timeToLive = created->mTimeToLive; +#else + Ogre::Vector3& position = created->position; + Ogre::Vector3& direction = created->direction; + Ogre::ColourValue& colour = created->colour; + float& totalTimeToLive = created->totalTimeToLive; + float& timeToLive = created->timeToLive; +#endif + + direction = particle.velocity; + position = particledata->vertices.at(particle.vertex); + + if (particle.vertex < int(particledata->colors.size())) + { + Ogre::Vector4 partcolour = particledata->colors.at(particle.vertex); + colour = Ogre::ColourValue(partcolour.x, partcolour.y, partcolour.z, partcolour.w); + } + else + colour = Ogre::ColourValue(1.f, 1.f, 1.f, 1.f); + float size = particledata->sizes.at(particle.vertex); + created->setDimensions(size, size); + totalTimeToLive = std::max(0.f, particle.lifespan); + timeToLive = std::max(0.f, particle.lifespan - particle.lifetime); + } + } static void createNodeControllers(const Nif::NIFFilePtr& nif, const std::string &name, Nif::ControllerPtr ctrl, ObjectScenePtr scene, int animflags) { @@ -1275,6 +1353,8 @@ ObjectScenePtr Loader::createObjects(Ogre::SceneNode *parentNode, std::string na parentNode->attachObject(entity); } + scene->_notifyAttached(); + return scene; } @@ -1342,6 +1422,8 @@ ObjectScenePtr Loader::createObjects(Ogre::Entity *parent, const std::string &bo } } + scene->_notifyAttached(); + return scene; } diff --git a/components/nifogre/ogrenifloader.hpp b/components/nifogre/ogrenifloader.hpp index abadd38de5..485495a388 100644 --- a/components/nifogre/ogrenifloader.hpp +++ b/components/nifogre/ogrenifloader.hpp @@ -84,6 +84,10 @@ struct ObjectScene { void rotateBillboardNodes(Ogre::Camera* camera); void setVisibilityFlags (unsigned int flags); + + // This is called internally by the OgreNifLoader once all elements of the + // scene have been attached to their respective nodes. + void _notifyAttached(); }; typedef Ogre::SharedPtr ObjectScenePtr; From 5a2564907634f37ff63b87b3bf752ce8ee5c1df6 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 5 Dec 2014 20:58:03 +0100 Subject: [PATCH 187/303] Implement XYZ rotation keys support (Fixes #1067) --- components/nif/data.hpp | 16 ++++++++-------- components/nif/nifkey.hpp | 6 ++---- components/nifogre/ogrenifloader.cpp | 21 +++++++++++++++++++++ 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/components/nif/data.hpp b/components/nif/data.hpp index 5efd0d6c99..d9de12fb54 100644 --- a/components/nif/data.hpp +++ b/components/nif/data.hpp @@ -350,8 +350,11 @@ struct NiMorphData : public Record struct NiKeyframeData : public Record { QuaternionKeyMap mRotations; - //\FIXME mXYZ_Keys are read, but not used. - FloatKeyMap mXYZ_Keys; + + FloatKeyMap mXRotations; + FloatKeyMap mYRotations; + FloatKeyMap mZRotations; + Vector3KeyMap mTranslations; FloatKeyMap mScales; @@ -362,12 +365,9 @@ struct NiKeyframeData : public Record { //Chomp unused float nif->getFloat(); - for(size_t i=0;i<3;++i) - { - //Read concatenates items together. - mXYZ_Keys.read(nif,true); - } - nif->file->warn("XYZ_ROTATION_KEY read, but not used!"); + mXRotations.read(nif, true); + mYRotations.read(nif, true); + mZRotations.read(nif, true); } mTranslations.read(nif); mScales.read(nif); diff --git a/components/nif/nifkey.hpp b/components/nif/nifkey.hpp index b0db80914f..cb81427201 100644 --- a/components/nif/nifkey.hpp +++ b/components/nif/nifkey.hpp @@ -49,9 +49,7 @@ struct KeyMapT { if(count == 0 && !force) return; - //If we aren't forcing things, make sure that read clears any previous keys - if(!force) - mKeys.clear(); + mKeys.clear(); mInterpolationType = nif->getUInt(); @@ -88,7 +86,7 @@ struct KeyMapT { //XYZ keys aren't actually read here. //data.hpp sees that the last type read was sXYZInterpolation and: // Eats a floating point number, then - // Re-runs the read function 3 more times, with force enabled so that the previous values aren't cleared. + // Re-runs the read function 3 more times. // When it does that it's reading in a bunch of sLinearInterpolation keys, not sXYZInterpolation. else if(mInterpolationType == sXYZInterpolation) { diff --git a/components/nifogre/ogrenifloader.cpp b/components/nifogre/ogrenifloader.cpp index b18a88d140..1d1e1a22d0 100644 --- a/components/nifogre/ogrenifloader.cpp +++ b/components/nifogre/ogrenifloader.cpp @@ -442,6 +442,9 @@ public: { private: const Nif::QuaternionKeyMap* mRotations; + const Nif::FloatKeyMap* mXRotations; + const Nif::FloatKeyMap* mYRotations; + const Nif::FloatKeyMap* mZRotations; const Nif::Vector3KeyMap* mTranslations; const Nif::FloatKeyMap* mScales; Nif::NIFFilePtr mNif; // Hold a SharedPtr to make sure key lists stay valid @@ -472,11 +475,25 @@ public: return keys.rbegin()->second.mValue; } + Ogre::Quaternion getXYZRotation(float time) const + { + float xrot = interpKey(mXRotations->mKeys, time); + float yrot = interpKey(mYRotations->mKeys, time); + float zrot = interpKey(mZRotations->mKeys, time); + Ogre::Quaternion xr(Ogre::Radian(xrot), Ogre::Vector3::UNIT_X); + Ogre::Quaternion yr(Ogre::Radian(yrot), Ogre::Vector3::UNIT_Y); + Ogre::Quaternion zr(Ogre::Radian(zrot), Ogre::Vector3::UNIT_Z); + return (xr*yr*zr); + } + public: /// @note The NiKeyFrameData must be valid as long as this KeyframeController exists. Value(Ogre::Node *target, const Nif::NIFFilePtr& nif, const Nif::NiKeyframeData *data) : NodeTargetValue(target) , mRotations(&data->mRotations) + , mXRotations(&data->mXRotations) + , mYRotations(&data->mYRotations) + , mZRotations(&data->mZRotations) , mTranslations(&data->mTranslations) , mScales(&data->mScales) , mNif(nif) @@ -486,6 +503,8 @@ public: { if(mRotations->mKeys.size() > 0) return interpKey(mRotations->mKeys, time); + else if (!mXRotations->mKeys.empty() || !mYRotations->mKeys.empty() || !mZRotations->mKeys.empty()) + return getXYZRotation(time); return mNode->getOrientation(); } @@ -513,6 +532,8 @@ public: { if(mRotations->mKeys.size() > 0) mNode->setOrientation(interpKey(mRotations->mKeys, time)); + else if (!mXRotations->mKeys.empty() || !mYRotations->mKeys.empty() || !mZRotations->mKeys.empty()) + mNode->setOrientation(getXYZRotation(time)); if(mTranslations->mKeys.size() > 0) mNode->setPosition(interpKey(mTranslations->mKeys, time)); if(mScales->mKeys.size() > 0) From e313ed3cefea5c41865d4d3d45f6abf8f9564312 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 5 Dec 2014 20:58:33 +0100 Subject: [PATCH 188/303] Support animated container models --- apps/openmw/mwclass/container.cpp | 7 +++++-- apps/openmw/mwrender/activatoranimation.cpp | 13 ++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index 179070aedf..59e51e4613 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -7,6 +7,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/failedaction.hpp" @@ -21,7 +22,7 @@ #include "../mwgui/tooltips.hpp" -#include "../mwrender/objects.hpp" +#include "../mwrender/actors.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwmechanics/npcstats.hpp" @@ -87,7 +88,8 @@ namespace MWClass { const std::string model = getModel(ptr); if (!model.empty()) { - renderingInterface.getObjects().insertModel(ptr, model); + MWRender::Actors& actors = renderingInterface.getActors(); + actors.insertActivator(ptr); } } @@ -96,6 +98,7 @@ namespace MWClass const std::string model = getModel(ptr); if(!model.empty()) physics.addObject(ptr); + MWBase::Environment::get().getMechanicsManager()->add(ptr); } std::string Container::getModel(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwrender/activatoranimation.cpp b/apps/openmw/mwrender/activatoranimation.cpp index de0457e578..540876a3ec 100644 --- a/apps/openmw/mwrender/activatoranimation.cpp +++ b/apps/openmw/mwrender/activatoranimation.cpp @@ -4,6 +4,8 @@ #include "../mwbase/world.hpp" +#include "../mwworld/class.hpp" + #include "renderconst.hpp" namespace MWRender @@ -16,17 +18,14 @@ ActivatorAnimation::~ActivatorAnimation() ActivatorAnimation::ActivatorAnimation(const MWWorld::Ptr &ptr) : Animation(ptr, ptr.getRefData().getBaseNode()) { - MWWorld::LiveCellRef *ref = mPtr.get(); + const std::string& model = mPtr.getClass().getModel(mPtr); - assert(ref->mBase != NULL); - if(!ref->mBase->mModel.empty()) + if(!model.empty()) { - const std::string name = "meshes\\"+ref->mBase->mModel; - - setObjectRoot(name, false); + setObjectRoot(model, false); setRenderProperties(mObjectRoot, RV_Misc, RQG_Main, RQG_Alpha); - addAnimSource(name); + addAnimSource(model); } } From 416d549568789db9eef0ff11278f6052b2530794 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 5 Dec 2014 22:02:18 +0100 Subject: [PATCH 189/303] Fix animation glitch caused by knockdown If the player was knocked down while having no weapon, spell nor fists ready, the animation state would incorrectly shift to "weapon equipped" even though no weapon is equipped. --- apps/openmw/mwmechanics/character.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 2e4f76c1a1..46e06f4604 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1072,7 +1072,8 @@ bool CharacterController::updateWeaponState() } else if (mHitState == CharState_KnockDown) { - mUpperBodyState = UpperCharState_WeapEquiped; + if (mUpperBodyState > UpperCharState_WeapEquiped) + mUpperBodyState = UpperCharState_WeapEquiped; mAnimation->disable(mCurrentWeapon); } } From 3f0bc6eecbef10d63fc54a3282c3b61823fd008f Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 5 Dec 2014 23:36:06 +0100 Subject: [PATCH 190/303] Ignore extra bytes after the SCVR string list (Fixes #2184) --- components/esm/loadscpt.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/components/esm/loadscpt.cpp b/components/esm/loadscpt.cpp index 19e3f3bb3a..07561c2eaf 100644 --- a/components/esm/loadscpt.cpp +++ b/components/esm/loadscpt.cpp @@ -30,7 +30,14 @@ void Script::load(ESMReader &esm) int s = mData.mStringTableSize; std::vector tmp (s); - esm.getHExact (&tmp[0], s); + // not using getHExact, vanilla doesn't seem to mind unused bytes at the end + esm.getSubHeader(); + int left = esm.getSubSize(); + if (left < s) + esm.fail("SCVR string list is smaller than specified"); + esm.getExact(&tmp[0], s); + if (left > s) + esm.skip(left-s); // skip the leftover junk // Set up the list of variable names mVarNames.resize(mData.mNumShorts + mData.mNumLongs + mData.mNumFloats); From 7c59ea629694fde2322d0c6b7082e1e6f5642a7b Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 6 Dec 2014 13:01:55 +0100 Subject: [PATCH 191/303] added specialised report table --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/view/tools/reportsubview.cpp | 36 ++++------------ apps/opencs/view/tools/reportsubview.hpp | 23 ++-------- apps/opencs/view/tools/reporttable.cpp | 53 ++++++++++++++++++++++++ apps/opencs/view/tools/reporttable.hpp | 44 ++++++++++++++++++++ 5 files changed, 108 insertions(+), 50 deletions(-) create mode 100644 apps/opencs/view/tools/reporttable.cpp create mode 100644 apps/opencs/view/tools/reporttable.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index bdfefbb83f..fcc549002a 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -92,7 +92,7 @@ opencs_hdrs_noqt (view/render opencs_units (view/tools - reportsubview + reportsubview reporttable ) opencs_units_noqt (view/tools diff --git a/apps/opencs/view/tools/reportsubview.cpp b/apps/opencs/view/tools/reportsubview.cpp index 5a2523789e..df1a5298cd 100644 --- a/apps/opencs/view/tools/reportsubview.cpp +++ b/apps/opencs/view/tools/reportsubview.cpp @@ -1,31 +1,15 @@ #include "reportsubview.hpp" -#include -#include - -#include "../../model/tools/reportmodel.hpp" - -#include "../../view/world/idtypedelegate.hpp" +#include "reporttable.hpp" CSVTools::ReportSubView::ReportSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) -: CSVDoc::SubView (id), mModel (document.getReport (id)) +: CSVDoc::SubView (id) { - setWidget (mTable = new QTableView (this)); - mTable->setModel (mModel); + setWidget (mTable = new ReportTable (document, id, this)); - mTable->horizontalHeader()->setResizeMode (QHeaderView::Interactive); - mTable->verticalHeader()->hide(); - mTable->setSortingEnabled (true); - mTable->setSelectionBehavior (QAbstractItemView::SelectRows); - mTable->setSelectionMode (QAbstractItemView::ExtendedSelection); - - mIdTypeDelegate = CSVWorld::IdTypeDelegateFactory().makeDelegate ( - document, this); - - mTable->setItemDelegateForColumn (0, mIdTypeDelegate); - - connect (mTable, SIGNAL (doubleClicked (const QModelIndex&)), this, SLOT (show (const QModelIndex&))); + connect (mTable, SIGNAL (editRequest (const CSMWorld::UniversalId&, const std::string&)), + SIGNAL (focusId (const CSMWorld::UniversalId&, const std::string&))); } void CSVTools::ReportSubView::setEditLock (bool locked) @@ -33,13 +17,7 @@ void CSVTools::ReportSubView::setEditLock (bool locked) // ignored. We don't change document state anyway. } -void CSVTools::ReportSubView::updateUserSetting - (const QString &name, const QStringList &list) +void CSVTools::ReportSubView::updateUserSetting (const QString &name, const QStringList &list) { - mIdTypeDelegate->updateUserSetting (name, list); -} - -void CSVTools::ReportSubView::show (const QModelIndex& index) -{ - focusId (mModel->getUniversalId (index.row()), ""); + mTable->updateUserSetting (name, list); } diff --git a/apps/opencs/view/tools/reportsubview.hpp b/apps/opencs/view/tools/reportsubview.hpp index 9f6a4c1da3..7e8a08e3cd 100644 --- a/apps/opencs/view/tools/reportsubview.hpp +++ b/apps/opencs/view/tools/reportsubview.hpp @@ -11,27 +11,15 @@ namespace CSMDoc class Document; } -namespace CSMTools -{ - class ReportModel; -} - -namespace CSVWorld -{ - class CommandDelegate; -} - namespace CSVTools { - class Table; + class ReportTable; class ReportSubView : public CSVDoc::SubView { Q_OBJECT - CSMTools::ReportModel *mModel; - QTableView *mTable; - CSVWorld::CommandDelegate *mIdTypeDelegate; + ReportTable *mTable; public: @@ -39,12 +27,7 @@ namespace CSVTools virtual void setEditLock (bool locked); - virtual void updateUserSetting - (const QString &, const QStringList &); - - private slots: - - void show (const QModelIndex& index); + virtual void updateUserSetting (const QString &, const QStringList &); }; } diff --git a/apps/opencs/view/tools/reporttable.cpp b/apps/opencs/view/tools/reporttable.cpp new file mode 100644 index 0000000000..cc0ced57fc --- /dev/null +++ b/apps/opencs/view/tools/reporttable.cpp @@ -0,0 +1,53 @@ + +#include "reporttable.hpp" + +#include + +#include "../../model/tools/reportmodel.hpp" + +#include "../../view/world/idtypedelegate.hpp" + +CSVTools::ReportTable::ReportTable (CSMDoc::Document& document, + const CSMWorld::UniversalId& id, QWidget *parent) +: CSVWorld::DragRecordTable (document, parent), mModel (document.getReport (id)) +{ + horizontalHeader()->setResizeMode (QHeaderView::Interactive); + verticalHeader()->hide(); + setSortingEnabled (true); + setSelectionBehavior (QAbstractItemView::SelectRows); + setSelectionMode (QAbstractItemView::ExtendedSelection); + + setModel (mModel); + + mIdTypeDelegate = CSVWorld::IdTypeDelegateFactory().makeDelegate ( + document, this); + + setItemDelegateForColumn (0, mIdTypeDelegate); + + connect (this, SIGNAL (doubleClicked (const QModelIndex&)), this, SLOT (show (const QModelIndex&))); +} + +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 (iter->row())); + } + + return ids; +} + +void CSVTools::ReportTable::updateUserSetting (const QString& name, const QStringList& list) +{ + mIdTypeDelegate->updateUserSetting (name, list); +} + +void CSVTools::ReportTable::show (const QModelIndex& index) +{ + emit editRequest (mModel->getUniversalId (index.row()), ""); +} \ No newline at end of file diff --git a/apps/opencs/view/tools/reporttable.hpp b/apps/opencs/view/tools/reporttable.hpp new file mode 100644 index 0000000000..18365f58ee --- /dev/null +++ b/apps/opencs/view/tools/reporttable.hpp @@ -0,0 +1,44 @@ +#ifndef CSV_TOOLS_REPORTTABLE_H +#define CSV_TOOLS_REPORTTABLE_H + +#include "../world/dragrecordtable.hpp" + +namespace CSMTools +{ + class ReportModel; +} + +namespace CSVWorld +{ + class CommandDelegate; +} + +namespace CSVTools +{ + class ReportTable : public CSVWorld::DragRecordTable + { + Q_OBJECT + + CSMTools::ReportModel *mModel; + CSVWorld::CommandDelegate *mIdTypeDelegate; + + public: + + ReportTable (CSMDoc::Document& document, const CSMWorld::UniversalId& id, + QWidget *parent = 0); + + virtual std::vector getDraggedRecords() const; + + void updateUserSetting (const QString& name, const QStringList& list); + + private slots: + + void show (const QModelIndex& index); + + signals: + + void editRequest (const CSMWorld::UniversalId& id, const std::string& hint); + }; +} + +#endif From 6c18a3b0b5d1fecb102eee68dbbeed441c4e6e99 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 6 Dec 2014 13:19:43 +0100 Subject: [PATCH 192/303] allow drags from report table --- apps/opencs/view/tools/reporttable.cpp | 6 ++++++ apps/opencs/view/tools/reporttable.hpp | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/apps/opencs/view/tools/reporttable.cpp b/apps/opencs/view/tools/reporttable.cpp index cc0ced57fc..e4acc26298 100644 --- a/apps/opencs/view/tools/reporttable.cpp +++ b/apps/opencs/view/tools/reporttable.cpp @@ -7,6 +7,12 @@ #include "../../view/world/idtypedelegate.hpp" +void CSVTools::ReportTable::mouseMoveEvent (QMouseEvent *event) +{ + if (event->buttons() & Qt::LeftButton) + startDrag (*this); +} + CSVTools::ReportTable::ReportTable (CSMDoc::Document& document, const CSMWorld::UniversalId& id, QWidget *parent) : CSVWorld::DragRecordTable (document, parent), mModel (document.getReport (id)) diff --git a/apps/opencs/view/tools/reporttable.hpp b/apps/opencs/view/tools/reporttable.hpp index 18365f58ee..acd1ed3cc7 100644 --- a/apps/opencs/view/tools/reporttable.hpp +++ b/apps/opencs/view/tools/reporttable.hpp @@ -22,6 +22,10 @@ namespace CSVTools CSMTools::ReportModel *mModel; CSVWorld::CommandDelegate *mIdTypeDelegate; + private: + + void mouseMoveEvent (QMouseEvent *event); + public: ReportTable (CSMDoc::Document& document, const CSMWorld::UniversalId& id, From 350b0cb93c7315dd3015b2688a500f7470644c88 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 6 Dec 2014 13:45:47 +0100 Subject: [PATCH 193/303] added hidden hint column to report model --- apps/opencs/model/tools/reportmodel.cpp | 27 +++++++++++++++++++------ apps/opencs/model/tools/reportmodel.hpp | 7 +++++-- apps/opencs/view/tools/reporttable.cpp | 3 ++- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/apps/opencs/model/tools/reportmodel.cpp b/apps/opencs/model/tools/reportmodel.cpp index 75545a7c7a..1354206128 100644 --- a/apps/opencs/model/tools/reportmodel.cpp +++ b/apps/opencs/model/tools/reportmodel.cpp @@ -16,7 +16,7 @@ int CSMTools::ReportModel::columnCount (const QModelIndex & parent) const if (parent.isValid()) return 0; - return 2; + return 3; } QVariant CSMTools::ReportModel::data (const QModelIndex & index, int role) const @@ -26,8 +26,11 @@ QVariant CSMTools::ReportModel::data (const QModelIndex & index, int role) const if (index.column()==0) return static_cast (mRows.at (index.row()).first.getType()); - else - return mRows.at (index.row()).second.c_str(); + + if (index.column()==1) + return QString::fromUtf8 (mRows.at (index.row()).second.first.c_str()); + + return QString::fromUtf8 (mRows.at (index.row()).second.second.c_str()); } QVariant CSMTools::ReportModel::headerData (int section, Qt::Orientation orientation, int role) const @@ -38,7 +41,13 @@ QVariant CSMTools::ReportModel::headerData (int section, Qt::Orientation orienta if (orientation==Qt::Vertical) return QVariant(); - return tr (section==0 ? "Type" : "Description"); + if (section==0) + return "Type"; + + if (section==1) + return "Description"; + + return "Hint"; } bool CSMTools::ReportModel::removeRows (int row, int count, const QModelIndex& parent) @@ -51,11 +60,12 @@ bool CSMTools::ReportModel::removeRows (int row, int count, const QModelIndex& p return true; } -void CSMTools::ReportModel::add (const CSMWorld::UniversalId& id, const std::string& message) +void CSMTools::ReportModel::add (const CSMWorld::UniversalId& id, const std::string& message, + const std::string& hint) { beginInsertRows (QModelIndex(), mRows.size(), mRows.size()); - mRows.push_back (std::make_pair (id, message)); + mRows.push_back (std::make_pair (id, std::make_pair (message, hint))); endInsertRows(); } @@ -64,3 +74,8 @@ const CSMWorld::UniversalId& CSMTools::ReportModel::getUniversalId (int row) con { return mRows.at (row).first; } + +std::string CSMTools::ReportModel::getHint (int row) const +{ + return mRows.at (row).second.second; +} \ No newline at end of file diff --git a/apps/opencs/model/tools/reportmodel.hpp b/apps/opencs/model/tools/reportmodel.hpp index 0f000245e1..709e024a72 100644 --- a/apps/opencs/model/tools/reportmodel.hpp +++ b/apps/opencs/model/tools/reportmodel.hpp @@ -14,7 +14,7 @@ namespace CSMTools { Q_OBJECT - std::vector > mRows; + std::vector > > mRows; public: @@ -28,9 +28,12 @@ namespace CSMTools virtual bool removeRows (int row, int count, const QModelIndex& parent = QModelIndex()); - void add (const CSMWorld::UniversalId& id, const std::string& message); + void add (const CSMWorld::UniversalId& id, const std::string& message, + const std::string& hint = ""); const CSMWorld::UniversalId& getUniversalId (int row) const; + + std::string getHint (int row) const; }; } diff --git a/apps/opencs/view/tools/reporttable.cpp b/apps/opencs/view/tools/reporttable.cpp index e4acc26298..5bf38f020f 100644 --- a/apps/opencs/view/tools/reporttable.cpp +++ b/apps/opencs/view/tools/reporttable.cpp @@ -24,6 +24,7 @@ CSVTools::ReportTable::ReportTable (CSMDoc::Document& document, setSelectionMode (QAbstractItemView::ExtendedSelection); setModel (mModel); + setColumnHidden (2, true); mIdTypeDelegate = CSVWorld::IdTypeDelegateFactory().makeDelegate ( document, this); @@ -55,5 +56,5 @@ void CSVTools::ReportTable::updateUserSetting (const QString& name, const QStrin void CSVTools::ReportTable::show (const QModelIndex& index) { - emit editRequest (mModel->getUniversalId (index.row()), ""); + emit editRequest (mModel->getUniversalId (index.row()), mModel->getHint (index.row())); } \ No newline at end of file From f2fc6933250d4cfa6b4a1459463657a349e00f76 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 6 Dec 2014 14:17:56 +0100 Subject: [PATCH 194/303] added context menu to report table --- apps/opencs/view/tools/reporttable.cpp | 28 ++++++++++++++++++++++++++ apps/opencs/view/tools/reporttable.hpp | 7 +++++++ 2 files changed, 35 insertions(+) diff --git a/apps/opencs/view/tools/reporttable.cpp b/apps/opencs/view/tools/reporttable.cpp index 5bf38f020f..ec2ba74d20 100644 --- a/apps/opencs/view/tools/reporttable.cpp +++ b/apps/opencs/view/tools/reporttable.cpp @@ -2,11 +2,26 @@ #include "reporttable.hpp" #include +#include +#include #include "../../model/tools/reportmodel.hpp" #include "../../view/world/idtypedelegate.hpp" +void CSVTools::ReportTable::contextMenuEvent (QContextMenuEvent *event) +{ + QModelIndexList selectedRows = selectionModel()->selectedRows(); + + // create context menu + QMenu menu (this); + + if (!selectedRows.empty()) + menu.addAction (mShowAction); + + menu.exec (event->globalPos()); +} + void CSVTools::ReportTable::mouseMoveEvent (QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) @@ -31,6 +46,10 @@ CSVTools::ReportTable::ReportTable (CSMDoc::Document& document, setItemDelegateForColumn (0, mIdTypeDelegate); + mShowAction = new QAction (tr ("Show"), this); + connect (mShowAction, SIGNAL (triggered()), this, SLOT (showSelection())); + addAction (mShowAction); + connect (this, SIGNAL (doubleClicked (const QModelIndex&)), this, SLOT (show (const QModelIndex&))); } @@ -57,4 +76,13 @@ void CSVTools::ReportTable::updateUserSetting (const QString& name, const QStrin void CSVTools::ReportTable::show (const QModelIndex& index) { emit editRequest (mModel->getUniversalId (index.row()), mModel->getHint (index.row())); +} + +void CSVTools::ReportTable::showSelection() +{ + QModelIndexList selectedRows = selectionModel()->selectedRows(); + + for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); + ++iter) + show (*iter); } \ No newline at end of file diff --git a/apps/opencs/view/tools/reporttable.hpp b/apps/opencs/view/tools/reporttable.hpp index acd1ed3cc7..75b6e85532 100644 --- a/apps/opencs/view/tools/reporttable.hpp +++ b/apps/opencs/view/tools/reporttable.hpp @@ -3,6 +3,8 @@ #include "../world/dragrecordtable.hpp" +class QAction; + namespace CSMTools { class ReportModel; @@ -21,9 +23,12 @@ namespace CSVTools CSMTools::ReportModel *mModel; CSVWorld::CommandDelegate *mIdTypeDelegate; + QAction *mShowAction; private: + void contextMenuEvent (QContextMenuEvent *event); + void mouseMoveEvent (QMouseEvent *event); public: @@ -39,6 +44,8 @@ namespace CSVTools void show (const QModelIndex& index); + void showSelection(); + signals: void editRequest (const CSMWorld::UniversalId& id, const std::string& hint); From 58f4cc882fdf80f46c4965d321f23d9f145058fa Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 6 Dec 2014 14:30:40 +0100 Subject: [PATCH 195/303] added remove action to report table context menu --- apps/opencs/view/tools/reporttable.cpp | 22 ++++++++++++++++++++++ apps/opencs/view/tools/reporttable.hpp | 3 +++ 2 files changed, 25 insertions(+) diff --git a/apps/opencs/view/tools/reporttable.cpp b/apps/opencs/view/tools/reporttable.cpp index ec2ba74d20..eee1da35f5 100644 --- a/apps/opencs/view/tools/reporttable.cpp +++ b/apps/opencs/view/tools/reporttable.cpp @@ -1,6 +1,8 @@ #include "reporttable.hpp" +#include + #include #include #include @@ -17,7 +19,10 @@ void CSVTools::ReportTable::contextMenuEvent (QContextMenuEvent *event) QMenu menu (this); if (!selectedRows.empty()) + { menu.addAction (mShowAction); + menu.addAction (mRemoveAction); + } menu.exec (event->globalPos()); } @@ -50,6 +55,10 @@ CSVTools::ReportTable::ReportTable (CSMDoc::Document& document, connect (mShowAction, SIGNAL (triggered()), this, SLOT (showSelection())); addAction (mShowAction); + mRemoveAction = new QAction (tr ("Remove from list"), this); + connect (mRemoveAction, SIGNAL (triggered()), this, SLOT (removeSelection())); + addAction (mRemoveAction); + connect (this, SIGNAL (doubleClicked (const QModelIndex&)), this, SLOT (show (const QModelIndex&))); } @@ -85,4 +94,17 @@ void CSVTools::ReportTable::showSelection() for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); ++iter) show (*iter); +} + +void CSVTools::ReportTable::removeSelection() +{ + QModelIndexList selectedRows = selectionModel()->selectedRows(); + + std::reverse (selectedRows.begin(), selectedRows.end()); + + for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); + ++iter) + mModel->removeRows (iter->row(), 1); + + selectionModel()->clear(); } \ No newline at end of file diff --git a/apps/opencs/view/tools/reporttable.hpp b/apps/opencs/view/tools/reporttable.hpp index 75b6e85532..2f53425d43 100644 --- a/apps/opencs/view/tools/reporttable.hpp +++ b/apps/opencs/view/tools/reporttable.hpp @@ -24,6 +24,7 @@ namespace CSVTools CSMTools::ReportModel *mModel; CSVWorld::CommandDelegate *mIdTypeDelegate; QAction *mShowAction; + QAction *mRemoveAction; private: @@ -46,6 +47,8 @@ namespace CSVTools void showSelection(); + void removeSelection(); + signals: void editRequest (const CSMWorld::UniversalId& id, const std::string& hint); From 6a67aba33655539c2bed83af06e269557473b7a5 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 6 Dec 2014 15:08:51 +0100 Subject: [PATCH 196/303] added double click with modifier actions in report table --- apps/opencs/view/tools/reporttable.cpp | 42 +++++++++++++++++++++----- apps/opencs/view/tools/reporttable.hpp | 4 +-- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/apps/opencs/view/tools/reporttable.cpp b/apps/opencs/view/tools/reporttable.cpp index eee1da35f5..4cd11925e0 100644 --- a/apps/opencs/view/tools/reporttable.cpp +++ b/apps/opencs/view/tools/reporttable.cpp @@ -33,6 +33,39 @@ void CSVTools::ReportTable::mouseMoveEvent (QMouseEvent *event) startDrag (*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); + + switch (modifiers) + { + case 0: + + event->accept(); + showSelection(); + break; + + case Qt::ShiftModifier: + + event->accept(); + removeSelection(); + break; + + case Qt::ControlModifier: + + event->accept(); + showSelection(); + removeSelection(); + break; + } +} + CSVTools::ReportTable::ReportTable (CSMDoc::Document& document, const CSMWorld::UniversalId& id, QWidget *parent) : CSVWorld::DragRecordTable (document, parent), mModel (document.getReport (id)) @@ -58,8 +91,6 @@ CSVTools::ReportTable::ReportTable (CSMDoc::Document& document, mRemoveAction = new QAction (tr ("Remove from list"), this); connect (mRemoveAction, SIGNAL (triggered()), this, SLOT (removeSelection())); addAction (mRemoveAction); - - connect (this, SIGNAL (doubleClicked (const QModelIndex&)), this, SLOT (show (const QModelIndex&))); } std::vector CSVTools::ReportTable::getDraggedRecords() const @@ -82,18 +113,13 @@ void CSVTools::ReportTable::updateUserSetting (const QString& name, const QStrin mIdTypeDelegate->updateUserSetting (name, list); } -void CSVTools::ReportTable::show (const QModelIndex& index) -{ - emit editRequest (mModel->getUniversalId (index.row()), mModel->getHint (index.row())); -} - void CSVTools::ReportTable::showSelection() { QModelIndexList selectedRows = selectionModel()->selectedRows(); for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); ++iter) - show (*iter); + emit editRequest (mModel->getUniversalId (iter->row()), mModel->getHint (iter->row())); } void CSVTools::ReportTable::removeSelection() diff --git a/apps/opencs/view/tools/reporttable.hpp b/apps/opencs/view/tools/reporttable.hpp index 2f53425d43..7a5b232f98 100644 --- a/apps/opencs/view/tools/reporttable.hpp +++ b/apps/opencs/view/tools/reporttable.hpp @@ -32,6 +32,8 @@ namespace CSVTools void mouseMoveEvent (QMouseEvent *event); + virtual void mouseDoubleClickEvent (QMouseEvent *event); + public: ReportTable (CSMDoc::Document& document, const CSMWorld::UniversalId& id, @@ -43,8 +45,6 @@ namespace CSVTools private slots: - void show (const QModelIndex& index); - void showSelection(); void removeSelection(); From 9a1b7cbe52c082c923d19bfdd87e623cbb70db27 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 6 Dec 2014 16:50:09 +0100 Subject: [PATCH 197/303] Add SharedStateButton, used in spell window and controls box to apply mouseover effect to all buttons within one row (Fixes #1986) --- apps/openmw/mwgui/settingswindow.cpp | 11 +- apps/openmw/mwgui/spellwindow.cpp | 42 +++++--- components/CMakeLists.txt | 2 +- components/widgets/sharedstatebutton.cpp | 128 +++++++++++++++++++++++ components/widgets/sharedstatebutton.hpp | 51 +++++++++ components/widgets/widgets.cpp | 2 + 6 files changed, 221 insertions(+), 15 deletions(-) create mode 100644 components/widgets/sharedstatebutton.cpp create mode 100644 components/widgets/sharedstatebutton.hpp diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 948423a356..8ef0a331a4 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -9,6 +9,8 @@ #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" @@ -454,16 +456,21 @@ namespace MWGui std::string binding = MWBase::Environment::get().getInputManager()->getActionBindingName (*it); - MyGUI::TextBox* leftText = mControlsBox->createWidget("SandText", MyGUI::IntCoord(0,curH,w,h), MyGUI::Align::Default); + Gui::SharedStateButton* leftText = mControlsBox->createWidget("SandTextButton", MyGUI::IntCoord(0,curH,w,h), MyGUI::Align::Default); leftText->setCaptionWithReplacing(desc); - MyGUI::Button* rightText = mControlsBox->createWidget("SandTextButton", MyGUI::IntCoord(0,curH,w,h), MyGUI::Align::Default); + Gui::SharedStateButton* rightText = mControlsBox->createWidget("SandTextButton", MyGUI::IntCoord(0,curH,w,h), MyGUI::Align::Default); rightText->setCaptionWithReplacing(binding); rightText->setTextAlign (MyGUI::Align::Right); rightText->setUserData(*it); // save the action id for callbacks rightText->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onRebindAction); rightText->eventMouseWheel += MyGUI::newDelegate(this, &SettingsWindow::onInputTabMouseWheel); curH += h; + + Gui::ButtonGroup group; + group.push_back(leftText); + group.push_back(rightText); + Gui::SharedStateButton::createButtonGroup(group); } // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index 97ebbcde2f..7904c249ab 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -177,7 +179,7 @@ namespace MWGui for (std::vector::const_iterator it = spellList.begin(); it != spellList.end(); ++it) { const ESM::Spell* spell = esmStore.get().find(*it); - MyGUI::Button* t = mSpellView->createWidget("SandTextButton", + Gui::SharedStateButton* t = mSpellView->createWidget("SandTextButton", MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); t->setCaption(spell->mName); t->setTextAlign(MyGUI::Align::Left); @@ -185,20 +187,28 @@ namespace MWGui t->setUserString("Spell", *it); t->eventMouseWheel += MyGUI::newDelegate(this, &SpellWindow::onMouseWheel); t->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellWindow::onSpellSelected); - t->setStateSelected(*it == MWBase::Environment::get().getWindowManager()->getSelectedSpell()); // cost / success chance - MyGUI::Button* costChance = mSpellView->createWidget("SandTextButton", + Gui::SharedStateButton* costChance = mSpellView->createWidget("SandTextButton", MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); std::string cost = boost::lexical_cast(spell->mData.mCost); std::string chance = boost::lexical_cast(int(MWMechanics::getSpellSuccessChance(*it, player))); costChance->setCaption(cost + "/" + chance); costChance->setTextAlign(MyGUI::Align::Right); - costChance->setNeedMouseFocus(false); - costChance->setStateSelected(*it == MWBase::Environment::get().getWindowManager()->getSelectedSpell()); + costChance->setUserString("ToolTipType", "Spell"); + costChance->setUserString("Spell", *it); + costChance->eventMouseWheel += MyGUI::newDelegate(this, &SpellWindow::onMouseWheel); + costChance->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellWindow::onSpellSelected); t->setSize(mWidth-12-costChance->getTextSize().width, t->getHeight()); + Gui::ButtonGroup group; + group.push_back(t); + group.push_back(costChance); + Gui::SharedStateButton::createButtonGroup(group); + + t->setStateSelected(*it == MWBase::Environment::get().getWindowManager()->getSelectedSpell()); + mHeight += spellHeight; } @@ -224,7 +234,7 @@ namespace MWGui } } - MyGUI::Button* t = mSpellView->createWidget(equipped ? "SandTextButton" : "SpellTextUnequipped", + Gui::SharedStateButton* t = mSpellView->createWidget(equipped ? "SandTextButton" : "SpellTextUnequipped", MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); t->setCaption(item.getClass().getName(item)); t->setTextAlign(MyGUI::Align::Left); @@ -233,12 +243,9 @@ namespace MWGui t->setUserString("Equipped", equipped ? "true" : "false"); t->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellWindow::onEnchantedItemSelected); t->eventMouseWheel += MyGUI::newDelegate(this, &SpellWindow::onMouseWheel); - if (store.getSelectedEnchantItem() != store.end()) - t->setStateSelected(item == *store.getSelectedEnchantItem()); - // cost / charge - MyGUI::Button* costCharge = mSpellView->createWidget(equipped ? "SandTextButton" : "SpellTextUnequipped", + Gui::SharedStateButton* costCharge = mSpellView->createWidget(equipped ? "SandTextButton" : "SpellTextUnequipped", MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); float enchantCost = enchant->mData.mCost; @@ -257,11 +264,22 @@ namespace MWGui charge = "100"; } + + costCharge->setUserData(item); + costCharge->setUserString("ToolTipType", "ItemPtr"); + costCharge->setUserString("Equipped", equipped ? "true" : "false"); + costCharge->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellWindow::onEnchantedItemSelected); + costCharge->eventMouseWheel += MyGUI::newDelegate(this, &SpellWindow::onMouseWheel); costCharge->setCaption(cost + "/" + charge); costCharge->setTextAlign(MyGUI::Align::Right); - costCharge->setNeedMouseFocus(false); + + Gui::ButtonGroup group; + group.push_back(t); + group.push_back(costCharge); + Gui::SharedStateButton::createButtonGroup(group); + if (store.getSelectedEnchantItem() != store.end()) - costCharge->setStateSelected(item == *store.getSelectedEnchantItem()); + t->setStateSelected(item == *store.getSelectedEnchantItem()); t->setSize(mWidth-12-costCharge->getTextSize().width, t->getHeight()); diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 6a1db371d1..234325718d 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -96,7 +96,7 @@ add_component_dir (ogreinit ) add_component_dir (widgets - box imagebutton tags list numericeditbox widgets + box imagebutton tags list numericeditbox sharedstatebutton widgets ) add_component_dir (fontloader diff --git a/components/widgets/sharedstatebutton.cpp b/components/widgets/sharedstatebutton.cpp new file mode 100644 index 0000000000..6859a3065a --- /dev/null +++ b/components/widgets/sharedstatebutton.cpp @@ -0,0 +1,128 @@ +#include "sharedstatebutton.hpp" + +namespace Gui +{ + + SharedStateButton::SharedStateButton() + : mIsMousePressed(false) + , mIsMouseFocus(false) + { + + } + + void SharedStateButton::shutdownOverride() + { + ButtonGroup group = mSharedWith; // make a copy so that we don't nuke the vector during iteration + for (ButtonGroup::iterator it = group.begin(); it != group.end(); ++it) + { + (*it)->shareStateWith(ButtonGroup()); + } + } + + void SharedStateButton::shareStateWith(ButtonGroup shared) + { + mSharedWith = shared; + } + + void SharedStateButton::onMouseButtonPressed(int _left, int _top, MyGUI::MouseButton _id) + { + mIsMousePressed = true; + Base::onMouseButtonPressed(_left, _top, _id); + updateButtonState(); + } + + void SharedStateButton::onMouseButtonReleased(int _left, int _top, MyGUI::MouseButton _id) + { + mIsMousePressed = false; + Base::onMouseButtonReleased(_left, _top, _id); + updateButtonState(); + } + + void SharedStateButton::onMouseSetFocus(MyGUI::Widget *_old) + { + mIsMouseFocus = true; + Base::onMouseSetFocus(_old); + updateButtonState(); + } + + void SharedStateButton::onMouseLostFocus(MyGUI::Widget *_new) + { + mIsMouseFocus = false; + Base::onMouseLostFocus(_new); + updateButtonState(); + } + + void SharedStateButton::baseUpdateEnable() + { + Base::baseUpdateEnable(); + updateButtonState(); + } + + void SharedStateButton::setStateSelected(bool _value) + { + Base::setStateSelected(_value); + updateButtonState(); + + for (ButtonGroup::iterator it = mSharedWith.begin(); it != mSharedWith.end(); ++it) + { + (*it)->MyGUI::Button::setStateSelected(getStateSelected()); + } + } + + bool SharedStateButton::_setState(const std::string &_value) + { + bool ret = _setWidgetState(_value); + if (ret) + { + for (ButtonGroup::iterator it = mSharedWith.begin(); it != mSharedWith.end(); ++it) + { + (*it)->_setWidgetState(_value); + } + } + return ret; + } + + void SharedStateButton::updateButtonState() + { + if (getStateSelected()) + { + if (!getInheritedEnabled()) + { + if (!_setState("disabled_checked")) + _setState("disabled"); + } + else if (mIsMousePressed) + { + if (!_setState("pushed_checked")) + _setState("pushed"); + } + else if (mIsMouseFocus) + { + if (!_setState("highlighted_checked")) + _setState("pushed"); + } + else + _setState("normal_checked"); + } + else + { + if (!getInheritedEnabled()) + _setState("disabled"); + else if (mIsMousePressed) + _setState("pushed"); + else if (mIsMouseFocus) + _setState("highlighted"); + else + _setState("normal"); + } + } + + void SharedStateButton::createButtonGroup(ButtonGroup group) + { + for (ButtonGroup::iterator it = group.begin(); it != group.end(); ++it) + { + (*it)->shareStateWith(group); + } + } + +} diff --git a/components/widgets/sharedstatebutton.hpp b/components/widgets/sharedstatebutton.hpp new file mode 100644 index 0000000000..3d7fbc84e4 --- /dev/null +++ b/components/widgets/sharedstatebutton.hpp @@ -0,0 +1,51 @@ +#ifndef OPENMW_WIDGETS_SHAREDSTATEBUTTON_HPP +#define OPENMW_WIDGETS_SHAREDSTATEBUTTON_HPP + +#include + +namespace Gui +{ + + class SharedStateButton; + + typedef std::vector ButtonGroup; + + /// @brief A button that applies its own state changes to other widgets, to do this you define it as part of a ButtonGroup. + class SharedStateButton : public MyGUI::Button + { + MYGUI_RTTI_DERIVED(SharedStateButton) + + public: + SharedStateButton(); + + protected: + void updateButtonState(); + + virtual void onMouseButtonPressed(int _left, int _top, MyGUI::MouseButton _id); + virtual void onMouseButtonReleased(int _left, int _top, MyGUI::MouseButton _id); + virtual void onMouseSetFocus(MyGUI::Widget* _old); + virtual void onMouseLostFocus(MyGUI::Widget* _new); + virtual void baseUpdateEnable(); + + virtual void shutdownOverride(); + + bool _setState(const std::string &_value); + + public: + void shareStateWith(ButtonGroup shared); + + /// @note The ButtonGroup connection will be destroyed when any widget in the group gets destroyed. + static void createButtonGroup(ButtonGroup group); + + //! Set button selected state + void setStateSelected(bool _value); + + private: + ButtonGroup mSharedWith; + + bool mIsMousePressed; + bool mIsMouseFocus; + }; +} + +#endif diff --git a/components/widgets/widgets.cpp b/components/widgets/widgets.cpp index b35dc88a4d..82839c6c96 100644 --- a/components/widgets/widgets.cpp +++ b/components/widgets/widgets.cpp @@ -6,6 +6,7 @@ #include "numericeditbox.hpp" #include "box.hpp" #include "imagebutton.hpp" +#include "sharedstatebutton.hpp" namespace Gui { @@ -20,6 +21,7 @@ namespace Gui MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); } } From 41542dedf7686bd222aa99df4270fc5b3f20e928 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 6 Dec 2014 17:24:05 +0100 Subject: [PATCH 198/303] Fix map insert return value mixup (Fixes #2192) --- apps/openmw/mwworld/store.hpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index 5966a99b9e..c8fa087c20 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -859,16 +859,16 @@ namespace MWWorld // Try to overwrite existing record if (!pathgrid.mCell.empty()) { - std::pair found = mInt.insert(std::make_pair(pathgrid.mCell, pathgrid)); - if (found.second) - found.first->second = pathgrid; + std::pair ret = mInt.insert(std::make_pair(pathgrid.mCell, pathgrid)); + if (!ret.second) + ret.first->second = pathgrid; } else { - std::pair found = mExt.insert(std::make_pair(std::make_pair(pathgrid.mData.mX, pathgrid.mData.mY), + std::pair ret = mExt.insert(std::make_pair(std::make_pair(pathgrid.mData.mX, pathgrid.mData.mY), pathgrid)); - if (found.second) - found.first->second = pathgrid; + if (!ret.second) + ret.first->second = pathgrid; } } @@ -953,9 +953,9 @@ namespace MWWorld record.load(esm); // Try to overwrite existing record - std::pair found = mStatic.insert(std::make_pair(record.mIndex, record)); - if (found.second) - found.first->second = record; + std::pair ret = mStatic.insert(std::make_pair(record.mIndex, record)); + if (!ret.second) + ret.first->second = record; } int getSize() const { From 2952a0e2aa7d9fa58bc37c5f235a61f28afd6a29 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 6 Dec 2014 19:53:24 +0100 Subject: [PATCH 199/303] Make Resurrect function reset most of the runtime state (Fixes #2181) --- apps/openmw/mwscript/statsextensions.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp index d5647db10d..63405e5b85 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -1179,7 +1179,14 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime) { MWWorld::Ptr ptr = R()(runtime); - ptr.getClass().getCreatureStats(ptr).resurrect(); + + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + ptr.getClass().getCreatureStats(ptr).resurrect(); + else if (ptr.getClass().getCreatureStats(ptr).isDead()) + { + // resets runtime state such as inventory, stats and AI. does not reset position in the world + ptr.getRefData().setCustomData(NULL); + } } }; From f49fde3d5de464c546aa41e69172c5063cf600dd Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 6 Dec 2014 21:08:18 +0100 Subject: [PATCH 200/303] Add support for undeleting references (Fixes #2193) Deleted references should be accessible via an explicit reference, and can be undeleted using "setdelete 0". Also the Resurrect function implicitely undeletes the given reference. --- apps/openmw/mwbase/world.hpp | 1 + apps/openmw/mwscript/miscextensions.cpp | 4 ++++ apps/openmw/mwscript/statsextensions.cpp | 1 + apps/openmw/mwworld/cellreflist.hpp | 4 ++-- apps/openmw/mwworld/cellstore.hpp | 2 +- apps/openmw/mwworld/refdata.cpp | 5 +++++ apps/openmw/mwworld/refdata.hpp | 2 ++ apps/openmw/mwworld/worldimp.cpp | 19 +++++++++++++++++++ apps/openmw/mwworld/worldimp.hpp | 1 + 9 files changed, 36 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index c1a889913a..2dd135f3da 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -279,6 +279,7 @@ namespace MWBase ///< Attempt to fix position so that the Ptr is no longer inside collision geometry. virtual void deleteObject (const MWWorld::Ptr& ptr) = 0; + virtual void undeleteObject (const MWWorld::Ptr& ptr) = 0; virtual void moveObject (const MWWorld::Ptr& ptr, float x, float y, float z) = 0; diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index aa80213de8..ec2048e11f 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -571,6 +571,10 @@ namespace MWScript if (parameter == 1) MWBase::Environment::get().getWorld()->deleteObject(ptr); + else if (parameter == 0) + MWBase::Environment::get().getWorld()->undeleteObject(ptr); + else + throw std::runtime_error("SetDelete: unexpected parameter"); } }; diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp index 63405e5b85..09ab0183c7 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -1184,6 +1184,7 @@ namespace MWScript ptr.getClass().getCreatureStats(ptr).resurrect(); else if (ptr.getClass().getCreatureStats(ptr).isDead()) { + MWBase::Environment::get().getWorld()->undeleteObject(ptr); // resets runtime state such as inventory, stats and AI. does not reset position in the world ptr.getRefData().setCustomData(NULL); } diff --git a/apps/openmw/mwworld/cellreflist.hpp b/apps/openmw/mwworld/cellreflist.hpp index 9c3370f08a..cf12896548 100644 --- a/apps/openmw/mwworld/cellreflist.hpp +++ b/apps/openmw/mwworld/cellreflist.hpp @@ -27,7 +27,7 @@ namespace MWWorld LiveRef *find (const std::string& name) { for (typename List::iterator iter (mList.begin()); iter!=mList.end(); ++iter) - if (iter->mData.getCount() > 0 && iter->mRef.getRefId() == name) + if (iter->mRef.getRefId() == name) return &*iter; return 0; @@ -42,7 +42,7 @@ namespace MWWorld LiveCellRef *searchViaHandle (const std::string& handle) { for (typename List::iterator iter (mList.begin()); iter!=mList.end(); ++iter) - if (iter->mData.getCount()>0 && iter->mData.getBaseNode() && + if (iter->mData.getBaseNode() && iter->mData.getHandle()==handle) return &*iter; diff --git a/apps/openmw/mwworld/cellstore.hpp b/apps/openmw/mwworld/cellstore.hpp index 05e7b0b2e0..eba627b3ee 100644 --- a/apps/openmw/mwworld/cellstore.hpp +++ b/apps/openmw/mwworld/cellstore.hpp @@ -196,7 +196,7 @@ namespace MWWorld for (typename List::List::iterator iter (list.mList.begin()); iter!=list.mList.end(); ++iter) { - if (iter->mData.isDeleted()) + if (iter->mData.isDeletedByContentFile()) continue; if (!functor (MWWorld::Ptr(&*iter, this))) return false; diff --git a/apps/openmw/mwworld/refdata.cpp b/apps/openmw/mwworld/refdata.cpp index 78fea2b868..c2a5e5f837 100644 --- a/apps/openmw/mwworld/refdata.cpp +++ b/apps/openmw/mwworld/refdata.cpp @@ -180,6 +180,11 @@ namespace MWWorld return mDeleted || mCount == 0; } + bool RefData::isDeletedByContentFile() const + { + return mDeleted; + } + MWScript::Locals& RefData::getLocals() { return mLocals; diff --git a/apps/openmw/mwworld/refdata.hpp b/apps/openmw/mwworld/refdata.hpp index 1ed3cd79d9..da7986ba03 100644 --- a/apps/openmw/mwworld/refdata.hpp +++ b/apps/openmw/mwworld/refdata.hpp @@ -100,6 +100,8 @@ namespace MWWorld /// 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(); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 7baf1d3e5c..d783857f10 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1072,6 +1072,25 @@ namespace MWWorld } } + void World::undeleteObject(const Ptr& ptr) + { + if (ptr.getCellRef().getRefNum().mContentFile == -1) + 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()); + } + } + } + void World::moveObject(const Ptr &ptr, CellStore* newCell, float x, float y, float z) { ESM::Position pos = ptr.getRefData().getPosition(); diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index fef2797050..2da6a6e05f 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -338,6 +338,7 @@ namespace MWWorld virtual std::pair getHitContact(const MWWorld::Ptr &ptr, float distance); virtual void deleteObject (const Ptr& ptr); + virtual void undeleteObject (const Ptr& ptr); virtual void moveObject (const Ptr& ptr, float x, float y, float z); virtual void moveObject (const Ptr& ptr, CellStore* newCell, float x, float y, float z); From 109fbab54688a65c500937993f39e87ee2a1e798 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 7 Dec 2014 16:02:28 +0100 Subject: [PATCH 201/303] changed column/row numbering in script compiler error messages from being starting at 0 to starting at 1 --- components/compiler/streamerrorhandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/compiler/streamerrorhandler.cpp b/components/compiler/streamerrorhandler.cpp index 8a74ad0863..fc1a059432 100644 --- a/components/compiler/streamerrorhandler.cpp +++ b/components/compiler/streamerrorhandler.cpp @@ -16,7 +16,7 @@ namespace Compiler mStream << "warning "; mStream - << "line " << loc.mLine << ", column " << loc.mColumn + << "line " << loc.mLine+1 << ", column " << loc.mColumn+1 << " (" << loc.mLiteral << ")" << std::endl << " " << message << std::endl; } From 3a847732b44048ab474330c7b6330974ce55b9a5 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 7 Dec 2014 18:57:47 +0100 Subject: [PATCH 202/303] abstracted message collection into a class --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/model/doc/loader.cpp | 8 +-- apps/opencs/model/doc/messages.cpp | 28 ++++++++++ apps/opencs/model/doc/messages.hpp | 44 +++++++++++++++ apps/opencs/model/doc/operation.cpp | 6 +-- apps/opencs/model/doc/stage.hpp | 4 +- apps/opencs/model/tools/birthsigncheck.cpp | 2 +- apps/opencs/model/tools/birthsigncheck.hpp | 2 +- apps/opencs/model/tools/bodypartcheck.cpp | 2 +- apps/opencs/model/tools/bodypartcheck.hpp | 2 +- apps/opencs/model/tools/classcheck.cpp | 2 +- apps/opencs/model/tools/classcheck.hpp | 2 +- apps/opencs/model/tools/factioncheck.cpp | 2 +- apps/opencs/model/tools/factioncheck.hpp | 2 +- apps/opencs/model/tools/mandatoryid.cpp | 5 +- apps/opencs/model/tools/mandatoryid.hpp | 2 +- apps/opencs/model/tools/racecheck.cpp | 6 +-- apps/opencs/model/tools/racecheck.hpp | 6 +-- .../opencs/model/tools/referenceablecheck.cpp | 54 +++++++++---------- .../opencs/model/tools/referenceablecheck.hpp | 54 +++++++++---------- apps/opencs/model/tools/regioncheck.cpp | 2 +- apps/opencs/model/tools/regioncheck.hpp | 2 +- apps/opencs/model/tools/scriptcheck.cpp | 2 +- apps/opencs/model/tools/scriptcheck.hpp | 4 +- apps/opencs/model/tools/skillcheck.cpp | 2 +- apps/opencs/model/tools/skillcheck.hpp | 2 +- apps/opencs/model/tools/soundcheck.cpp | 2 +- apps/opencs/model/tools/soundcheck.hpp | 2 +- apps/opencs/model/tools/spellcheck.cpp | 2 +- apps/opencs/model/tools/spellcheck.hpp | 2 +- apps/opencs/model/world/data.cpp | 13 +++-- apps/opencs/model/world/data.hpp | 2 +- apps/opencs/model/world/refcollection.cpp | 5 +- apps/opencs/model/world/refcollection.hpp | 2 +- 34 files changed, 174 insertions(+), 105 deletions(-) create mode 100644 apps/opencs/model/doc/messages.cpp create mode 100644 apps/opencs/model/doc/messages.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index fcc549002a..468e172cd2 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -11,7 +11,7 @@ opencs_units (model/doc ) opencs_units_noqt (model/doc - stage savingstate savingstages blacklist + stage savingstate savingstages blacklist messages ) opencs_hdrs_noqt (model/doc diff --git a/apps/opencs/model/doc/loader.cpp b/apps/opencs/model/doc/loader.cpp index 712deb9dfc..43f3b850ea 100644 --- a/apps/opencs/model/doc/loader.cpp +++ b/apps/opencs/model/doc/loader.cpp @@ -52,7 +52,7 @@ void CSMDoc::Loader::load() { if (iter->second.mRecordsLeft) { - CSMDoc::Stage::Messages messages; + CSMDoc::Messages messages; for (int i=0; igetData().continueLoading (messages)) { @@ -65,11 +65,11 @@ void CSMDoc::Loader::load() CSMWorld::UniversalId log (CSMWorld::UniversalId::Type_LoadErrorLog, 0); { // silence a g++ warning - for (CSMDoc::Stage::Messages::const_iterator iter (messages.begin()); + for (CSMDoc::Messages::Iterator iter (messages.begin()); iter!=messages.end(); ++iter) { - document->getReport (log)->add (iter->first, iter->second); - emit loadMessage (document, iter->second); + document->getReport (log)->add (iter->mId, iter->mMessage); + emit loadMessage (document, iter->mMessage); } } diff --git a/apps/opencs/model/doc/messages.cpp b/apps/opencs/model/doc/messages.cpp new file mode 100644 index 0000000000..1fb423145a --- /dev/null +++ b/apps/opencs/model/doc/messages.cpp @@ -0,0 +1,28 @@ + +#include "messages.hpp" + +void CSMDoc::Messages::add (const CSMWorld::UniversalId& id, const std::string& message, + const std::string& hint) +{ + Message data; + data.mId = id; + data.mMessage = message; + data.mHint = hint; + + mMessages.push_back (data); +} + +void CSMDoc::Messages::push_back (const std::pair& data) +{ + add (data.first, data.second); +} + +CSMDoc::Messages::Iterator CSMDoc::Messages::begin() const +{ + return mMessages.begin(); +} + +CSMDoc::Messages::Iterator CSMDoc::Messages::end() const +{ + return mMessages.end(); +} \ No newline at end of file diff --git a/apps/opencs/model/doc/messages.hpp b/apps/opencs/model/doc/messages.hpp new file mode 100644 index 0000000000..0f36c73a73 --- /dev/null +++ b/apps/opencs/model/doc/messages.hpp @@ -0,0 +1,44 @@ +#ifndef CSM_DOC_MESSAGES_H +#define CSM_DOC_MESSAGES_H + +#include +#include + +#include "../world/universalid.hpp" + +namespace CSMDoc +{ + class Messages + { + public: + + struct Message + { + CSMWorld::UniversalId mId; + std::string mMessage; + std::string mHint; + }; + + typedef std::vector Collection; + + typedef Collection::const_iterator Iterator; + + private: + + Collection mMessages; + + public: + + void add (const CSMWorld::UniversalId& id, const std::string& message, + const std::string& hint = ""); + + /// \deprecated Use add instead. + void push_back (const std::pair& data); + + Iterator begin() const; + + Iterator end() const; + }; +} + +#endif diff --git a/apps/opencs/model/doc/operation.cpp b/apps/opencs/model/doc/operation.cpp index 7f77e8ac9a..a997579f83 100644 --- a/apps/opencs/model/doc/operation.cpp +++ b/apps/opencs/model/doc/operation.cpp @@ -84,7 +84,7 @@ void CSMDoc::Operation::abort() void CSMDoc::Operation::executeStage() { - Stage::Messages messages; + Messages messages; while (mCurrentStage!=mStages.end()) { @@ -112,8 +112,8 @@ void CSMDoc::Operation::executeStage() emit progress (mCurrentStepTotal, mTotalSteps ? mTotalSteps : 1, mType); - for (Stage::Messages::const_iterator iter (messages.begin()); iter!=messages.end(); ++iter) - emit reportMessage (iter->first, iter->second, mType); + for (Messages::Iterator iter (messages.begin()); iter!=messages.end(); ++iter) + emit reportMessage (iter->mId, iter->mMessage, mType); if (mCurrentStage==mStages.end()) exit(); diff --git a/apps/opencs/model/doc/stage.hpp b/apps/opencs/model/doc/stage.hpp index ca34c22299..126823ae91 100644 --- a/apps/opencs/model/doc/stage.hpp +++ b/apps/opencs/model/doc/stage.hpp @@ -6,14 +6,14 @@ #include "../world/universalid.hpp" +#include "messages.hpp" + namespace CSMDoc { class Stage { public: - typedef std::vector > Messages; - virtual ~Stage(); virtual int setup() = 0; diff --git a/apps/opencs/model/tools/birthsigncheck.cpp b/apps/opencs/model/tools/birthsigncheck.cpp index db20ce4bcd..1d72e24b87 100644 --- a/apps/opencs/model/tools/birthsigncheck.cpp +++ b/apps/opencs/model/tools/birthsigncheck.cpp @@ -17,7 +17,7 @@ int CSMTools::BirthsignCheckStage::setup() return mBirthsigns.getSize(); } -void CSMTools::BirthsignCheckStage::perform (int stage, Messages& messages) +void CSMTools::BirthsignCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mBirthsigns.getRecord (stage); diff --git a/apps/opencs/model/tools/birthsigncheck.hpp b/apps/opencs/model/tools/birthsigncheck.hpp index 1030e5c021..16d4c666fd 100644 --- a/apps/opencs/model/tools/birthsigncheck.hpp +++ b/apps/opencs/model/tools/birthsigncheck.hpp @@ -21,7 +21,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/bodypartcheck.cpp b/apps/opencs/model/tools/bodypartcheck.cpp index a26945acf6..68a09485f2 100644 --- a/apps/opencs/model/tools/bodypartcheck.cpp +++ b/apps/opencs/model/tools/bodypartcheck.cpp @@ -14,7 +14,7 @@ int CSMTools::BodyPartCheckStage::setup() return mBodyParts.getSize(); } -void CSMTools::BodyPartCheckStage::perform ( int stage, Messages &messages ) +void CSMTools::BodyPartCheckStage::perform (int stage, CSMDoc::Messages &messages) { const CSMWorld::Record &record = mBodyParts.getRecord(stage); diff --git a/apps/opencs/model/tools/bodypartcheck.hpp b/apps/opencs/model/tools/bodypartcheck.hpp index d72badfdf7..0a6ca959ac 100644 --- a/apps/opencs/model/tools/bodypartcheck.hpp +++ b/apps/opencs/model/tools/bodypartcheck.hpp @@ -27,7 +27,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform( int stage, Messages &messages ); + virtual void perform( int stage, CSMDoc::Messages &messages ); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/classcheck.cpp b/apps/opencs/model/tools/classcheck.cpp index cea4f3a686..5b872a2668 100644 --- a/apps/opencs/model/tools/classcheck.cpp +++ b/apps/opencs/model/tools/classcheck.cpp @@ -18,7 +18,7 @@ int CSMTools::ClassCheckStage::setup() return mClasses.getSize(); } -void CSMTools::ClassCheckStage::perform (int stage, Messages& messages) +void CSMTools::ClassCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mClasses.getRecord (stage); diff --git a/apps/opencs/model/tools/classcheck.hpp b/apps/opencs/model/tools/classcheck.hpp index ec50ba35d1..b76da3f13d 100644 --- a/apps/opencs/model/tools/classcheck.hpp +++ b/apps/opencs/model/tools/classcheck.hpp @@ -21,7 +21,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/factioncheck.cpp b/apps/opencs/model/tools/factioncheck.cpp index ba8cfe1f9b..0dfdee7754 100644 --- a/apps/opencs/model/tools/factioncheck.cpp +++ b/apps/opencs/model/tools/factioncheck.cpp @@ -18,7 +18,7 @@ int CSMTools::FactionCheckStage::setup() return mFactions.getSize(); } -void CSMTools::FactionCheckStage::perform (int stage, Messages& messages) +void CSMTools::FactionCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mFactions.getRecord (stage); diff --git a/apps/opencs/model/tools/factioncheck.hpp b/apps/opencs/model/tools/factioncheck.hpp index ccc44e6a92..321a4d6d80 100644 --- a/apps/opencs/model/tools/factioncheck.hpp +++ b/apps/opencs/model/tools/factioncheck.hpp @@ -21,7 +21,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/mandatoryid.cpp b/apps/opencs/model/tools/mandatoryid.cpp index 412e9f2f02..87d19401ba 100644 --- a/apps/opencs/model/tools/mandatoryid.cpp +++ b/apps/opencs/model/tools/mandatoryid.cpp @@ -15,10 +15,9 @@ int CSMTools::MandatoryIdStage::setup() return mIds.size(); } -void CSMTools::MandatoryIdStage::perform (int stage, Messages& messages) +void CSMTools::MandatoryIdStage::perform (int stage, CSMDoc::Messages& messages) { if (mIdCollection.searchId (mIds.at (stage))==-1 || mIdCollection.getRecord (mIds.at (stage)).isDeleted()) - messages.push_back (std::make_pair (mCollectionId, - "Missing mandatory record: " + mIds.at (stage))); + messages.add (mCollectionId, "Missing mandatory record: " + mIds.at (stage)); } \ No newline at end of file diff --git a/apps/opencs/model/tools/mandatoryid.hpp b/apps/opencs/model/tools/mandatoryid.hpp index a8afea62af..86015c9824 100644 --- a/apps/opencs/model/tools/mandatoryid.hpp +++ b/apps/opencs/model/tools/mandatoryid.hpp @@ -30,7 +30,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/racecheck.cpp b/apps/opencs/model/tools/racecheck.cpp index 47aeda1e69..143d617721 100644 --- a/apps/opencs/model/tools/racecheck.cpp +++ b/apps/opencs/model/tools/racecheck.cpp @@ -7,7 +7,7 @@ #include "../world/universalid.hpp" -void CSMTools::RaceCheckStage::performPerRecord (int stage, Messages& messages) +void CSMTools::RaceCheckStage::performPerRecord (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mRaces.getRecord (stage); @@ -46,7 +46,7 @@ void CSMTools::RaceCheckStage::performPerRecord (int stage, Messages& messages) /// \todo check data members that can't be edited in the table view } -void CSMTools::RaceCheckStage::performFinal (Messages& messages) +void CSMTools::RaceCheckStage::performFinal (CSMDoc::Messages& messages) { CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Races); @@ -64,7 +64,7 @@ int CSMTools::RaceCheckStage::setup() return mRaces.getSize()+1; } -void CSMTools::RaceCheckStage::perform (int stage, Messages& messages) +void CSMTools::RaceCheckStage::perform (int stage, CSMDoc::Messages& messages) { if (stage==mRaces.getSize()) performFinal (messages); diff --git a/apps/opencs/model/tools/racecheck.hpp b/apps/opencs/model/tools/racecheck.hpp index c68b283be4..3e67b75771 100644 --- a/apps/opencs/model/tools/racecheck.hpp +++ b/apps/opencs/model/tools/racecheck.hpp @@ -15,9 +15,9 @@ namespace CSMTools const CSMWorld::IdCollection& mRaces; bool mPlayable; - void performPerRecord (int stage, Messages& messages); + void performPerRecord (int stage, CSMDoc::Messages& messages); - void performFinal (Messages& messages); + void performFinal (CSMDoc::Messages& messages); public: @@ -26,7 +26,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/referenceablecheck.cpp b/apps/opencs/model/tools/referenceablecheck.cpp index 1816d0808a..5190aacd59 100644 --- a/apps/opencs/model/tools/referenceablecheck.cpp +++ b/apps/opencs/model/tools/referenceablecheck.cpp @@ -18,7 +18,7 @@ CSMTools::ReferenceableCheckStage::ReferenceableCheckStage( { } -void CSMTools::ReferenceableCheckStage::perform (int stage, Messages& messages) +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()); @@ -232,7 +232,7 @@ int CSMTools::ReferenceableCheckStage::setup() void CSMTools::ReferenceableCheckStage::bookCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Book >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -250,7 +250,7 @@ void CSMTools::ReferenceableCheckStage::bookCheck( void CSMTools::ReferenceableCheckStage::activatorCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Activator >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -270,7 +270,7 @@ void CSMTools::ReferenceableCheckStage::activatorCheck( void CSMTools::ReferenceableCheckStage::potionCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Potion >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -290,7 +290,7 @@ void CSMTools::ReferenceableCheckStage::potionCheck( void CSMTools::ReferenceableCheckStage::apparatusCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Apparatus >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -310,7 +310,7 @@ void CSMTools::ReferenceableCheckStage::apparatusCheck( void CSMTools::ReferenceableCheckStage::armorCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Armor >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -336,7 +336,7 @@ void CSMTools::ReferenceableCheckStage::armorCheck( void CSMTools::ReferenceableCheckStage::clothingCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Clothing >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -353,7 +353,7 @@ void CSMTools::ReferenceableCheckStage::clothingCheck( void CSMTools::ReferenceableCheckStage::containerCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Container >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -381,7 +381,7 @@ void CSMTools::ReferenceableCheckStage::containerCheck( void CSMTools::ReferenceableCheckStage::creatureCheck ( int stage, const CSMWorld::RefIdDataContainer< ESM::Creature >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -448,7 +448,7 @@ void CSMTools::ReferenceableCheckStage::creatureCheck ( void CSMTools::ReferenceableCheckStage::doorCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Door >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -469,7 +469,7 @@ void CSMTools::ReferenceableCheckStage::doorCheck( void CSMTools::ReferenceableCheckStage::ingredientCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Ingredient >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -487,7 +487,7 @@ void CSMTools::ReferenceableCheckStage::ingredientCheck( void CSMTools::ReferenceableCheckStage::creaturesLevListCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::CreatureLevList >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -505,7 +505,7 @@ void CSMTools::ReferenceableCheckStage::creaturesLevListCheck( void CSMTools::ReferenceableCheckStage::itemLevelledListCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::ItemLevList >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -522,7 +522,7 @@ void CSMTools::ReferenceableCheckStage::itemLevelledListCheck( void CSMTools::ReferenceableCheckStage::lightCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Light >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -547,7 +547,7 @@ void CSMTools::ReferenceableCheckStage::lightCheck( void CSMTools::ReferenceableCheckStage::lockpickCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Lockpick >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -567,7 +567,7 @@ void CSMTools::ReferenceableCheckStage::lockpickCheck( void CSMTools::ReferenceableCheckStage::miscCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Miscellaneous >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -584,7 +584,7 @@ void CSMTools::ReferenceableCheckStage::miscCheck( void CSMTools::ReferenceableCheckStage::npcCheck ( int stage, const CSMWorld::RefIdDataContainer< ESM::NPC >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -701,7 +701,7 @@ void CSMTools::ReferenceableCheckStage::npcCheck ( void CSMTools::ReferenceableCheckStage::weaponCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Weapon >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); @@ -778,7 +778,7 @@ void CSMTools::ReferenceableCheckStage::weaponCheck( void CSMTools::ReferenceableCheckStage::probeCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Probe >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -796,7 +796,7 @@ void CSMTools::ReferenceableCheckStage::probeCheck( void CSMTools::ReferenceableCheckStage::repairCheck ( int stage, const CSMWorld::RefIdDataContainer< ESM::Repair >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); @@ -812,7 +812,7 @@ void CSMTools::ReferenceableCheckStage::repairCheck ( void CSMTools::ReferenceableCheckStage::staticCheck ( int stage, const CSMWorld::RefIdDataContainer< ESM::Static >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); @@ -828,7 +828,7 @@ void CSMTools::ReferenceableCheckStage::staticCheck ( //final check -void CSMTools::ReferenceableCheckStage::finalCheck (Messages& messages) +void CSMTools::ReferenceableCheckStage::finalCheck (CSMDoc::Messages& messages) { if (!mPlayerPresent) messages.push_back (std::make_pair (CSMWorld::UniversalId::Type_Referenceables, @@ -839,7 +839,7 @@ void CSMTools::ReferenceableCheckStage::finalCheck (Messages& messages) //Templates begins here template void CSMTools::ReferenceableCheckStage::inventoryItemCheck ( - const Item& someItem, Messages& messages, const std::string& someID, bool enchantable) + const Item& someItem, CSMDoc::Messages& messages, const std::string& someID, bool enchantable) { if (someItem.mName.empty()) messages.push_back (std::make_pair (someID, someItem.mId + " has an empty name")); @@ -865,7 +865,7 @@ template void CSMTools::ReferenceableCheckStage::inventoryItemChe } template void CSMTools::ReferenceableCheckStage::inventoryItemCheck ( - const Item& someItem, Messages& messages, const std::string& someID) + const Item& someItem, CSMDoc::Messages& messages, const std::string& someID) { if (someItem.mName.empty()) messages.push_back (std::make_pair (someID, someItem.mId + " has an empty name")); @@ -888,7 +888,7 @@ template void CSMTools::ReferenceableCheckStage::inventoryItemChe } template void CSMTools::ReferenceableCheckStage::toolCheck ( - const Tool& someTool, Messages& messages, const std::string& someID, bool canBeBroken) + const Tool& someTool, CSMDoc::Messages& messages, const std::string& someID, bool canBeBroken) { if (someTool.mData.mQuality <= 0) messages.push_back (std::make_pair (someID, someTool.mId + " has non-positive quality")); @@ -899,14 +899,14 @@ template void CSMTools::ReferenceableCheckStage::toolCheck ( } template void CSMTools::ReferenceableCheckStage::toolCheck ( - const Tool& someTool, Messages& messages, const std::string& someID) + const Tool& someTool, CSMDoc::Messages& messages, const std::string& someID) { if (someTool.mData.mQuality <= 0) messages.push_back (std::make_pair (someID, someTool.mId + " has non-positive quality")); } template void CSMTools::ReferenceableCheckStage::listCheck ( - const List& someList, Messages& messages, const std::string& someID) + const List& someList, CSMDoc::Messages& messages, const std::string& someID) { for (unsigned i = 0; i < someList.mList.size(); ++i) { diff --git a/apps/opencs/model/tools/referenceablecheck.hpp b/apps/opencs/model/tools/referenceablecheck.hpp index b0129fc2a5..ac7ed70821 100644 --- a/apps/opencs/model/tools/referenceablecheck.hpp +++ b/apps/opencs/model/tools/referenceablecheck.hpp @@ -17,56 +17,56 @@ namespace CSMTools const CSMWorld::IdCollection& classes, const CSMWorld::IdCollection& factions); - virtual void perform(int stage, Messages& messages); + virtual void perform(int stage, CSMDoc::Messages& messages); virtual int setup(); private: //CONCRETE CHECKS - void bookCheck(int stage, const CSMWorld::RefIdDataContainer< ESM::Book >& records, Messages& messages); - void activatorCheck(int stage, const CSMWorld::RefIdDataContainer< ESM::Activator >& records, Messages& messages); - void potionCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void apparatusCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void armorCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void clothingCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void containerCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void creatureCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void doorCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void ingredientCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void creaturesLevListCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void itemLevelledListCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void lightCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void lockpickCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void miscCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void npcCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void weaponCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void probeCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void repairCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void staticCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); + 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 (Messages& messages); + void finalCheck (CSMDoc::Messages& messages); //TEMPLATE CHECKS template void inventoryItemCheck(const ITEM& someItem, - Messages& messages, + CSMDoc::Messages& messages, const std::string& someID, bool enchantable); //for all enchantable items. template void inventoryItemCheck(const ITEM& someItem, - Messages& messages, + CSMDoc::Messages& messages, const std::string& someID); //for non-enchantable items. template void toolCheck(const TOOL& someTool, - Messages& messages, + CSMDoc::Messages& messages, const std::string& someID, bool canbebroken); //for tools with uses. template void toolCheck(const TOOL& someTool, - Messages& messages, + CSMDoc::Messages& messages, const std::string& someID); //for tools without uses. template void listCheck(const LIST& someList, - Messages& messages, + CSMDoc::Messages& messages, const std::string& someID); const CSMWorld::RefIdData& mReferencables; diff --git a/apps/opencs/model/tools/regioncheck.cpp b/apps/opencs/model/tools/regioncheck.cpp index 07df204701..091836d0d7 100644 --- a/apps/opencs/model/tools/regioncheck.cpp +++ b/apps/opencs/model/tools/regioncheck.cpp @@ -17,7 +17,7 @@ int CSMTools::RegionCheckStage::setup() return mRegions.getSize(); } -void CSMTools::RegionCheckStage::perform (int stage, Messages& messages) +void CSMTools::RegionCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mRegions.getRecord (stage); diff --git a/apps/opencs/model/tools/regioncheck.hpp b/apps/opencs/model/tools/regioncheck.hpp index a12903e7d4..8ba32e1377 100644 --- a/apps/opencs/model/tools/regioncheck.hpp +++ b/apps/opencs/model/tools/regioncheck.hpp @@ -21,7 +21,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/scriptcheck.cpp b/apps/opencs/model/tools/scriptcheck.cpp index d2c647bdab..fe371a3689 100644 --- a/apps/opencs/model/tools/scriptcheck.cpp +++ b/apps/opencs/model/tools/scriptcheck.cpp @@ -58,7 +58,7 @@ int CSMTools::ScriptCheckStage::setup() return mDocument.getData().getScripts().getSize(); } -void CSMTools::ScriptCheckStage::perform (int stage, Messages& messages) +void CSMTools::ScriptCheckStage::perform (int stage, CSMDoc::Messages& messages) { mId = mDocument.getData().getScripts().getId (stage); diff --git a/apps/opencs/model/tools/scriptcheck.hpp b/apps/opencs/model/tools/scriptcheck.hpp index 75f11b9d4f..3fe12fc9a4 100644 --- a/apps/opencs/model/tools/scriptcheck.hpp +++ b/apps/opencs/model/tools/scriptcheck.hpp @@ -23,7 +23,7 @@ namespace CSMTools CSMWorld::ScriptContext mContext; std::string mId; std::string mFile; - Messages *mMessages; + CSMDoc::Messages *mMessages; virtual void report (const std::string& message, const Compiler::TokenLoc& loc, Type type); ///< Report error to the user. @@ -38,7 +38,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/skillcheck.cpp b/apps/opencs/model/tools/skillcheck.cpp index 630516c72a..e061e042cc 100644 --- a/apps/opencs/model/tools/skillcheck.cpp +++ b/apps/opencs/model/tools/skillcheck.cpp @@ -16,7 +16,7 @@ int CSMTools::SkillCheckStage::setup() return mSkills.getSize(); } -void CSMTools::SkillCheckStage::perform (int stage, Messages& messages) +void CSMTools::SkillCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSkills.getRecord (stage); diff --git a/apps/opencs/model/tools/skillcheck.hpp b/apps/opencs/model/tools/skillcheck.hpp index cf5d53b5c9..93b06fe71f 100644 --- a/apps/opencs/model/tools/skillcheck.hpp +++ b/apps/opencs/model/tools/skillcheck.hpp @@ -21,7 +21,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/soundcheck.cpp b/apps/opencs/model/tools/soundcheck.cpp index 3d222e9092..e122ced915 100644 --- a/apps/opencs/model/tools/soundcheck.cpp +++ b/apps/opencs/model/tools/soundcheck.cpp @@ -16,7 +16,7 @@ int CSMTools::SoundCheckStage::setup() return mSounds.getSize(); } -void CSMTools::SoundCheckStage::perform (int stage, Messages& messages) +void CSMTools::SoundCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSounds.getRecord (stage); diff --git a/apps/opencs/model/tools/soundcheck.hpp b/apps/opencs/model/tools/soundcheck.hpp index a82a0eb6d7..52f2d3714a 100644 --- a/apps/opencs/model/tools/soundcheck.hpp +++ b/apps/opencs/model/tools/soundcheck.hpp @@ -21,7 +21,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/spellcheck.cpp b/apps/opencs/model/tools/spellcheck.cpp index 3d0be46fdf..0b59dc862a 100644 --- a/apps/opencs/model/tools/spellcheck.cpp +++ b/apps/opencs/model/tools/spellcheck.cpp @@ -17,7 +17,7 @@ int CSMTools::SpellCheckStage::setup() return mSpells.getSize(); } -void CSMTools::SpellCheckStage::perform (int stage, Messages& messages) +void CSMTools::SpellCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSpells.getRecord (stage); diff --git a/apps/opencs/model/tools/spellcheck.hpp b/apps/opencs/model/tools/spellcheck.hpp index 182f1888b2..9c3ea88855 100644 --- a/apps/opencs/model/tools/spellcheck.hpp +++ b/apps/opencs/model/tools/spellcheck.hpp @@ -21,7 +21,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index f6bd8e13b9..67f6822c7c 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -671,7 +671,7 @@ int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base return mReader->getRecordCount(); } -bool CSMWorld::Data::continueLoading (CSMDoc::Stage::Messages& messages) +bool CSMWorld::Data::continueLoading (CSMDoc::Messages& messages) { if (!mReader) throw std::logic_error ("can't continue loading, because no load has been started"); @@ -794,8 +794,8 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Stage::Messages& messages) } else { - messages.push_back (std::make_pair (UniversalId::Type_None, - "Trying to delete dialogue record " + id + " which does not exist")); + messages.add (UniversalId::Type_None, + "Trying to delete dialogue record " + id + " which does not exist"); } } else @@ -811,8 +811,8 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Stage::Messages& messages) { if (!mDialogue) { - messages.push_back (std::make_pair (UniversalId::Type_None, - "Found info record not following a dialogue record")); + messages.add (UniversalId::Type_None, + "Found info record not following a dialogue record"); mReader->skipRecord(); break; @@ -855,8 +855,7 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Stage::Messages& messages) if (unhandledRecord) { - messages.push_back (std::make_pair (UniversalId::Type_None, - "Unsupported record type: " + n.toString())); + messages.add (UniversalId::Type_None, "Unsupported record type: " + n.toString()); mReader->skipRecord(); } diff --git a/apps/opencs/model/world/data.hpp b/apps/opencs/model/world/data.hpp index 37d4d4b8a7..02f7bc4526 100644 --- a/apps/opencs/model/world/data.hpp +++ b/apps/opencs/model/world/data.hpp @@ -244,7 +244,7 @@ namespace CSMWorld /// ///< \return estimated number of records - bool continueLoading (CSMDoc::Stage::Messages& messages); + bool continueLoading (CSMDoc::Messages& messages); ///< \return Finished? bool hasId (const std::string& id) const; diff --git a/apps/opencs/model/world/refcollection.cpp b/apps/opencs/model/world/refcollection.cpp index c516e2c3ec..47f0276c6c 100644 --- a/apps/opencs/model/world/refcollection.cpp +++ b/apps/opencs/model/world/refcollection.cpp @@ -11,7 +11,7 @@ #include "record.hpp" void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool base, - std::map& cache, CSMDoc::Stage::Messages& messages) + std::map& cache, CSMDoc::Messages& messages) { Record cell = mCells.getRecord (cellIndex); @@ -36,8 +36,7 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Cell, mCells.getId (cellIndex)); - messages.push_back (std::make_pair (id, - "Attempt to delete a non-existing reference")); + messages.add (id, "Attempt to delete a non-existing reference"); continue; } diff --git a/apps/opencs/model/world/refcollection.hpp b/apps/opencs/model/world/refcollection.hpp index 63d369ed98..4ecc32b2f9 100644 --- a/apps/opencs/model/world/refcollection.hpp +++ b/apps/opencs/model/world/refcollection.hpp @@ -28,7 +28,7 @@ namespace CSMWorld void load (ESM::ESMReader& reader, int cellIndex, bool base, std::map& cache, - CSMDoc::Stage::Messages& messages); + CSMDoc::Messages& messages); ///< Load a sequence of references. std::string getNewId(); From a64b741af209c88d2f4269f528525690266e0a66 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 7 Dec 2014 20:53:09 +0100 Subject: [PATCH 203/303] store hints from operations in report model --- apps/opencs/model/doc/document.cpp | 6 +++--- apps/opencs/model/doc/document.hpp | 2 +- apps/opencs/model/doc/operation.cpp | 4 ++-- apps/opencs/model/doc/operation.hpp | 2 +- apps/opencs/model/tools/tools.cpp | 8 ++++---- apps/opencs/model/tools/tools.hpp | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/opencs/model/doc/document.cpp b/apps/opencs/model/doc/document.cpp index 4abd67a505..e688a9474a 100644 --- a/apps/opencs/model/doc/document.cpp +++ b/apps/opencs/model/doc/document.cpp @@ -2301,8 +2301,8 @@ CSMDoc::Document::Document (const Files::ConfigurationManager& configuration, connect (&mSaving, SIGNAL (done (int, bool)), this, SLOT (operationDone (int, bool))); connect ( - &mSaving, SIGNAL (reportMessage (const CSMWorld::UniversalId&, const std::string&, int)), - this, SLOT (reportMessage (const CSMWorld::UniversalId&, const std::string&, int))); + &mSaving, SIGNAL (reportMessage (const CSMWorld::UniversalId&, const std::string&, const std::string&, int)), + this, SLOT (reportMessage (const CSMWorld::UniversalId&, const std::string&, const std::string&, int))); connect (&mRunner, SIGNAL (runStateChanged()), this, SLOT (runStateChanged())); } @@ -2387,7 +2387,7 @@ void CSMDoc::Document::modificationStateChanged (bool clean) } void CSMDoc::Document::reportMessage (const CSMWorld::UniversalId& id, const std::string& message, - int type) + const std::string& hint, int type) { /// \todo find a better way to get these messages to the user. std::cout << message << std::endl; diff --git a/apps/opencs/model/doc/document.hpp b/apps/opencs/model/doc/document.hpp index e5aa5eea58..f3aef6db63 100644 --- a/apps/opencs/model/doc/document.hpp +++ b/apps/opencs/model/doc/document.hpp @@ -149,7 +149,7 @@ namespace CSMDoc void modificationStateChanged (bool clean); void reportMessage (const CSMWorld::UniversalId& id, const std::string& message, - int type); + const std::string& hint, int type); void operationDone (int type, bool failed); diff --git a/apps/opencs/model/doc/operation.cpp b/apps/opencs/model/doc/operation.cpp index a997579f83..e728050f4f 100644 --- a/apps/opencs/model/doc/operation.cpp +++ b/apps/opencs/model/doc/operation.cpp @@ -101,7 +101,7 @@ void CSMDoc::Operation::executeStage() } catch (const std::exception& e) { - emit reportMessage (CSMWorld::UniversalId(), e.what(), mType); + emit reportMessage (CSMWorld::UniversalId(), e.what(), "", mType); abort(); } @@ -113,7 +113,7 @@ void CSMDoc::Operation::executeStage() emit progress (mCurrentStepTotal, mTotalSteps ? mTotalSteps : 1, mType); for (Messages::Iterator iter (messages.begin()); iter!=messages.end(); ++iter) - emit reportMessage (iter->mId, iter->mMessage, mType); + emit reportMessage (iter->mId, iter->mMessage, iter->mHint, mType); if (mCurrentStage==mStages.end()) exit(); diff --git a/apps/opencs/model/doc/operation.hpp b/apps/opencs/model/doc/operation.hpp index d5a7d4e098..3c94677545 100644 --- a/apps/opencs/model/doc/operation.hpp +++ b/apps/opencs/model/doc/operation.hpp @@ -52,7 +52,7 @@ namespace CSMDoc void progress (int current, int max, int type); void reportMessage (const CSMWorld::UniversalId& id, const std::string& message, - int type); + const std::string& hint, int type); void done (int type, bool failed); diff --git a/apps/opencs/model/tools/tools.cpp b/apps/opencs/model/tools/tools.cpp index 79b0b18b0e..6e157f664f 100644 --- a/apps/opencs/model/tools/tools.cpp +++ b/apps/opencs/model/tools/tools.cpp @@ -48,8 +48,8 @@ CSMDoc::Operation *CSMTools::Tools::getVerifier() 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 CSMWorld::UniversalId&, const std::string&, int)), - this, SLOT (verifierMessage (const CSMWorld::UniversalId&, const std::string&, int))); + SIGNAL (reportMessage (const CSMWorld::UniversalId&, const std::string&, const std::string&, int)), + this, SLOT (verifierMessage (const CSMWorld::UniversalId&, const std::string&, const std::string&, int))); std::vector mandatoryIds; // I want C++11, damn it! mandatoryIds.push_back ("Day"); @@ -155,11 +155,11 @@ CSMTools::ReportModel *CSMTools::Tools::getReport (const CSMWorld::UniversalId& } void CSMTools::Tools::verifierMessage (const CSMWorld::UniversalId& id, const std::string& message, - int type) + const std::string& hint, int type) { std::map::iterator iter = mActiveReports.find (type); if (iter!=mActiveReports.end()) - mReports[iter->second]->add (id, message); + mReports[iter->second]->add (id, message, hint); } diff --git a/apps/opencs/model/tools/tools.hpp b/apps/opencs/model/tools/tools.hpp index 7ca30e6b97..5125a36381 100644 --- a/apps/opencs/model/tools/tools.hpp +++ b/apps/opencs/model/tools/tools.hpp @@ -64,7 +64,7 @@ namespace CSMTools private slots: void verifierMessage (const CSMWorld::UniversalId& id, const std::string& message, - int type); + const std::string& hint, int type); signals: From 9f90a1e44bba06c581eb423172fe0648d4a646e9 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sun, 7 Dec 2014 22:37:50 +0100 Subject: [PATCH 204/303] Remove script access to deleted references that have no content file In original MW these objects are permanently deleted and can not be accessed anymore. --- apps/openmw/mwworld/cellreflist.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwworld/cellreflist.hpp b/apps/openmw/mwworld/cellreflist.hpp index cf12896548..037de8645e 100644 --- a/apps/openmw/mwworld/cellreflist.hpp +++ b/apps/openmw/mwworld/cellreflist.hpp @@ -27,7 +27,9 @@ namespace MWWorld LiveRef *find (const std::string& name) { for (typename List::iterator iter (mList.begin()); iter!=mList.end(); ++iter) - if (iter->mRef.getRefId() == name) + if (!iter->mData.isDeletedByContentFile() + && (iter->mRef.getRefNum().mContentFile != -1 || iter->mData.getCount() > 0) + && iter->mRef.getRefId() == name) return &*iter; return 0; From e6307a5151abff084e4bc09be6b66ccef8fef9a0 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Mon, 8 Dec 2014 12:29:23 +0100 Subject: [PATCH 205/303] move cursor in scripteditor to position of error --- apps/opencs/model/tools/scriptcheck.cpp | 6 +++++- apps/opencs/view/world/scriptsubview.cpp | 26 ++++++++++++++++++++++++ apps/opencs/view/world/scriptsubview.hpp | 2 ++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/opencs/model/tools/scriptcheck.cpp b/apps/opencs/model/tools/scriptcheck.cpp index fe371a3689..d9dea7f43c 100644 --- a/apps/opencs/model/tools/scriptcheck.cpp +++ b/apps/opencs/model/tools/scriptcheck.cpp @@ -28,7 +28,11 @@ void CSMTools::ScriptCheckStage::report (const std::string& message, const Compi << ", line " << loc.mLine << ", column " << loc.mColumn << " (" << loc.mLiteral << "): " << message; - mMessages->push_back (std::make_pair (id, stream.str())); + std::ostringstream hintStream; + + hintStream << "l:" << loc.mLine << " " << loc.mColumn; + + mMessages->add (id, stream.str(), hintStream.str()); } void CSMTools::ScriptCheckStage::report (const std::string& message, Type type) diff --git a/apps/opencs/view/world/scriptsubview.cpp b/apps/opencs/view/world/scriptsubview.cpp index 22d8e7e51e..9b50a61f89 100644 --- a/apps/opencs/view/world/scriptsubview.cpp +++ b/apps/opencs/view/world/scriptsubview.cpp @@ -47,6 +47,32 @@ void CSVWorld::ScriptSubView::setEditLock (bool locked) mEditor->setReadOnly (locked); } +void CSVWorld::ScriptSubView::useHint (const std::string& hint) +{ + if (hint.empty()) + return; + + if (hint[0]=='l') + { + std::istringstream stream (hint.c_str()+1); + + char ignore; + int line; + int column; + + if (stream >> ignore >> line >> 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->setTextCursor (cursor); + } + } +} + void CSVWorld::ScriptSubView::textChanged() { if (mEditor->isChangeLocked()) diff --git a/apps/opencs/view/world/scriptsubview.hpp b/apps/opencs/view/world/scriptsubview.hpp index 77127d9bee..16ffc7b80b 100644 --- a/apps/opencs/view/world/scriptsubview.hpp +++ b/apps/opencs/view/world/scriptsubview.hpp @@ -34,6 +34,8 @@ namespace CSVWorld virtual void setEditLock (bool locked); + virtual void useHint (const std::string& hint); + public slots: void textChanged(); From fbed429b257503417ecc32ef26f03c90a728a0d5 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 8 Dec 2014 17:24:45 +0100 Subject: [PATCH 206/303] Use GMSTs for sound fading distance --- apps/openmw/mwsound/soundmanagerimp.cpp | 36 ++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index 781bfb5d5a..251b05890b 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -103,24 +103,31 @@ namespace MWSound std::string SoundManager::lookup(const std::string &soundId, float &volume, float &min, float &max) { - const ESM::Sound *snd = - MWBase::Environment::get().getWorld()->getStore().get().find(soundId); + MWBase::World* world = MWBase::Environment::get().getWorld(); + const ESM::Sound *snd = world->getStore().get().find(soundId); volume *= pow(10.0, (snd->mData.mVolume/255.0*3348.0 - 3348.0) / 2000.0); if(snd->mData.mMinRange == 0 && snd->mData.mMaxRange == 0) { - min = 100.0f; - max = 2000.0f; + static const float fAudioDefaultMinDistance = world->getStore().get().find("fAudioDefaultMinDistance")->getFloat(); + static const float fAudioDefaultMaxDistance = world->getStore().get().find("fAudioDefaultMaxDistance")->getFloat(); + min = fAudioDefaultMinDistance; + max = fAudioDefaultMaxDistance; } else { - min = snd->mData.mMinRange * 20.0f; - max = snd->mData.mMaxRange * 50.0f; - min = std::max(min, 1.0f); - max = std::max(min, max); + min = snd->mData.mMinRange; + max = snd->mData.mMaxRange; } + static const float fAudioMinDistanceMult = world->getStore().get().find("fAudioMinDistanceMult")->getFloat(); + static const float fAudioMaxDistanceMult = world->getStore().get().find("fAudioMaxDistanceMult")->getFloat(); + min *= fAudioMinDistanceMult; + max *= fAudioMaxDistanceMult; + min = std::max(min, 1.0f); + max = std::max(min, max); + return "Sound/"+snd->mSound; } @@ -250,8 +257,19 @@ namespace MWSound const ESM::Position &pos = ptr.getRefData().getPosition(); const Ogre::Vector3 objpos(pos.pos); + MWBase::World* world = MWBase::Environment::get().getWorld(); + static const float fAudioMinDistanceMult = world->getStore().get().find("fAudioMinDistanceMult")->getFloat(); + static const float fAudioMaxDistanceMult = world->getStore().get().find("fAudioMaxDistanceMult")->getFloat(); + static const float fAudioVoiceDefaultMinDistance = world->getStore().get().find("fAudioVoiceDefaultMinDistance")->getFloat(); + static const float fAudioVoiceDefaultMaxDistance = world->getStore().get().find("fAudioVoiceDefaultMaxDistance")->getFloat(); + + float minDistance = fAudioVoiceDefaultMinDistance * fAudioMinDistanceMult; + float maxDistance = fAudioVoiceDefaultMaxDistance * fAudioMaxDistanceMult; + minDistance = std::max(minDistance, 1.f); + maxDistance = std::max(minDistance, maxDistance); + MWBase::SoundPtr sound = mOutput->playSound3D(filePath, objpos, 1.0f, basevol, 1.0f, - 20.0f, 1500.0f, Play_Normal|Play_TypeVoice, 0, true); + minDistance, maxDistance, Play_Normal|Play_TypeVoice, 0, true); mActiveSounds[sound] = std::make_pair(ptr, std::string("_say_sound")); } catch(std::exception &e) From cf85cbbc8e48f790e41faad5a765d94219debaef Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 8 Dec 2014 17:43:56 +0100 Subject: [PATCH 207/303] Switch sound distance model to AL_INVERSE_DISTANCE --- apps/openmw/mwsound/openal_output.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwsound/openal_output.cpp b/apps/openmw/mwsound/openal_output.cpp index 3d2795ce10..fcdc60ee39 100644 --- a/apps/openmw/mwsound/openal_output.cpp +++ b/apps/openmw/mwsound/openal_output.cpp @@ -628,9 +628,7 @@ void OpenAL_Sound3D::update() { ALfloat gain = mVolume*mBaseVolume; ALfloat pitch = mPitch; - if(mPos.squaredDistance(mOutput.mPos) > mMaxDistance*mMaxDistance) - gain = 0.0f; - else if(!(mFlags&MWBase::SoundManager::Play_NoEnv) && mOutput.mLastEnvironment == Env_Underwater) + if(!(mFlags&MWBase::SoundManager::Play_NoEnv) && mOutput.mLastEnvironment == Env_Underwater) { gain *= 0.9f; pitch *= 0.7f; @@ -696,7 +694,7 @@ void OpenAL_Output::init(const std::string &devname) fail(std::string("Failed to setup context: ")+alcGetString(mDevice, alcGetError(mDevice))); } - alDistanceModel(AL_LINEAR_DISTANCE_CLAMPED); + alDistanceModel(AL_INVERSE_DISTANCE); throwALerror(); ALCint maxmono=0, maxstereo=0; From 6c8a662042786171d3d17a6178d69a7f7e05533d Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Mon, 8 Dec 2014 20:08:59 +0100 Subject: [PATCH 208/303] label local/global openmw.cfg files (Fixes #2196) --- files/openmw.cfg | 4 ++++ files/openmw.cfg.local | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/files/openmw.cfg b/files/openmw.cfg index b60c1ba728..3a9bd87628 100644 --- a/files/openmw.cfg +++ b/files/openmw.cfg @@ -1,3 +1,7 @@ +# This is the global openmw.cfg file. Do not modify! +# Modifications should be done on the user openmw.cfg file instead +# (see: https://wiki.openmw.org/index.php?title=Paths) + data="?mw?Data Files" data=${MORROWIND_DATA_FILES} data-local="?userdata?data" diff --git a/files/openmw.cfg.local b/files/openmw.cfg.local index 9d86174d9e..71cd3bfbfa 100644 --- a/files/openmw.cfg.local +++ b/files/openmw.cfg.local @@ -1,3 +1,7 @@ +# This is the local openmw.cfg file. Do not modify! +# Modifications should be done on the user openmw.cfg file instead +# (see: https://wiki.openmw.org/index.php?title=Paths) + data="?global?data" data="?mw?Data Files" data=./data From ddad963312d4d21113be21792c9703655c071cd8 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Mon, 8 Dec 2014 20:14:00 +0100 Subject: [PATCH 209/303] adjusted changelog (removed a regression that was specific to 0.34.0) --- readme.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/readme.txt b/readme.txt index 91ec7962f1..e91455aebe 100644 --- a/readme.txt +++ b/readme.txt @@ -160,7 +160,6 @@ Bug #2161: Editor: combat/magic/stealth values of creature not displayed correct 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 #2173: Launcher: disabling plugin files is broken Bug #2175: Pathgrid mods do not overwrite the existing pathgrid Bug #2180: Editor: Verifier doesn't handle Windows-specific path issues when dealing with resources Feature #238: Add UI to run INI-importer from the launcher From f6960debcbf5d6b3758a6dca8da35c4e89daee1c Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 8 Dec 2014 23:26:09 +0100 Subject: [PATCH 210/303] Attach sound listener to the player head instead of camera --- apps/openmw/mwrender/camera.cpp | 11 ----------- apps/openmw/mwrender/camera.hpp | 3 --- apps/openmw/mwworld/worldimp.cpp | 13 +++++++++++++ apps/openmw/mwworld/worldimp.hpp | 1 + 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index 1850df904b..c7a27dfe8f 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -7,7 +7,6 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwbase/soundmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/refdata.hpp" @@ -120,15 +119,6 @@ namespace MWRender setPosition(Ogre::Vector3(x,y,z)); } - void Camera::updateListener() - { - Ogre::Vector3 pos = mCamera->getRealPosition(); - Ogre::Vector3 dir = mCamera->getRealDirection(); - Ogre::Vector3 up = mCamera->getRealUp(); - - MWBase::Environment::get().getSoundManager()->setListenerPosDir(pos, dir, up); - } - void Camera::update(float duration, bool paused) { if (mAnimation->upperBodyReady()) @@ -148,7 +138,6 @@ namespace MWRender } } - updateListener(); if (paused) return; diff --git a/apps/openmw/mwrender/camera.hpp b/apps/openmw/mwrender/camera.hpp index c542dc96ce..691a80862b 100644 --- a/apps/openmw/mwrender/camera.hpp +++ b/apps/openmw/mwrender/camera.hpp @@ -50,9 +50,6 @@ namespace MWRender bool mVanityToggleQueued; bool mViewModeToggleQueued; - /// Updates sound manager listener data - void updateListener(); - void setPosition(const Ogre::Vector3& position); void setPosition(float x, float y, float z); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index d783857f10..1b5d6e002f 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1560,6 +1560,8 @@ namespace MWWorld updateWindowManager (); + updateSoundListener(); + if (!paused && mPlayer->getPlayer().getCell()->isExterior()) { ESM::Position pos = mPlayer->getPlayer().getRefData().getPosition(); @@ -1567,6 +1569,17 @@ namespace MWWorld } } + void World::updateSoundListener() + { + Ogre::Vector3 playerPos = mPlayer->getPlayer().getRefData().getBaseNode()->getPosition(); + const OEngine::Physic::PhysicActor *actor = mPhysEngine->getCharacter(getPlayerPtr().getRefData().getHandle()); + if(actor) playerPos.z += 1.85*actor->getHalfExtents().z; + Ogre::Quaternion playerOrient = Ogre::Quaternion(Ogre::Radian(getPlayerPtr().getRefData().getPosition().rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z) * + Ogre::Quaternion(Ogre::Radian(getPlayerPtr().getRefData().getPosition().rot[0]), Ogre::Vector3::NEGATIVE_UNIT_X); + MWBase::Environment::get().getSoundManager()->setListenerPosDir(playerPos, playerOrient.yAxis(), + playerOrient.zAxis()); + } + void World::updateWindowManager () { // inform the GUI about focused object diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 2da6a6e05f..555ed7fb74 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -109,6 +109,7 @@ namespace MWWorld Ptr copyObjectToCell(const Ptr &ptr, CellStore* cell, ESM::Position pos, bool adjustPos=true); + void updateSoundListener(); void updateWindowManager (); void performUpdateSceneQueries (); void getFacedHandle(std::string& facedHandle, float maxDistance, bool ignorePlayer=true); From 855fe33c59458b2abc05887ace95733ebc44e1cb Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 8 Dec 2014 23:26:54 +0100 Subject: [PATCH 211/303] Add vanilla-compatible range limiting for playloopsound (Fixes #244, Fixes #1342) --- apps/openmw/mwbase/soundmanager.hpp | 20 +++++++++++++------- apps/openmw/mwscript/soundextensions.cpp | 8 ++++---- apps/openmw/mwsound/openal_output.cpp | 6 ++++-- apps/openmw/mwsound/soundmanagerimp.cpp | 12 ++++++++++++ 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/apps/openmw/mwbase/soundmanager.hpp b/apps/openmw/mwbase/soundmanager.hpp index a02a463dde..bc2f3f1c61 100644 --- a/apps/openmw/mwbase/soundmanager.hpp +++ b/apps/openmw/mwbase/soundmanager.hpp @@ -42,15 +42,21 @@ namespace MWBase Play_NoTrack = 1<<2, /* (3D only) Play the sound at the given object's position * but do not keep it updated (the sound will not move with * the object and will not stop when the object is deleted. */ - - Play_LoopNoEnv = Play_Loop | Play_NoEnv + Play_RemoveAtDistance = 1<<3, /* (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. */ + Play_LoopNoEnv = Play_Loop | Play_NoEnv, + Play_LoopRemoveAtDistance = Play_Loop | Play_RemoveAtDistance }; enum PlayType { - Play_TypeSfx = 1<<3, /* Normal SFX sound */ - Play_TypeVoice = 1<<4, /* Voice sound */ - Play_TypeFoot = 1<<5, /* Footstep sound */ - Play_TypeMusic = 1<<6, /* Music track */ - Play_TypeMovie = 1<<7, /* Movie audio track */ + Play_TypeSfx = 1<<4, /* Normal SFX sound */ + Play_TypeVoice = 1<<5, /* Voice sound */ + Play_TypeFoot = 1<<6, /* Footstep sound */ + Play_TypeMusic = 1<<7, /* Music track */ + Play_TypeMovie = 1<<8, /* Movie audio track */ Play_TypeMask = Play_TypeSfx|Play_TypeVoice|Play_TypeFoot|Play_TypeMusic|Play_TypeMovie }; diff --git a/apps/openmw/mwscript/soundextensions.cpp b/apps/openmw/mwscript/soundextensions.cpp index 73c3ec93a5..606de7aa01 100644 --- a/apps/openmw/mwscript/soundextensions.cpp +++ b/apps/openmw/mwscript/soundextensions.cpp @@ -121,8 +121,8 @@ namespace MWScript MWBase::Environment::get().getSoundManager()->playSound3D(ptr, sound, 1.0, 1.0, MWBase::SoundManager::Play_TypeSfx, - mLoop ? MWBase::SoundManager::Play_Loop : - MWBase::SoundManager::Play_Normal); + mLoop ? MWBase::SoundManager::Play_LoopRemoveAtDistance + : MWBase::SoundManager::Play_Normal); } }; @@ -150,8 +150,8 @@ namespace MWScript MWBase::Environment::get().getSoundManager()->playSound3D(ptr, sound, volume, pitch, MWBase::SoundManager::Play_TypeSfx, - mLoop ? MWBase::SoundManager::Play_Loop : - MWBase::SoundManager::Play_Normal); + mLoop ? MWBase::SoundManager::Play_LoopRemoveAtDistance + : MWBase::SoundManager::Play_Normal); } }; diff --git a/apps/openmw/mwsound/openal_output.cpp b/apps/openmw/mwsound/openal_output.cpp index fcdc60ee39..bc94789456 100644 --- a/apps/openmw/mwsound/openal_output.cpp +++ b/apps/openmw/mwsound/openal_output.cpp @@ -628,7 +628,9 @@ void OpenAL_Sound3D::update() { ALfloat gain = mVolume*mBaseVolume; ALfloat pitch = mPitch; - if(!(mFlags&MWBase::SoundManager::Play_NoEnv) && mOutput.mLastEnvironment == Env_Underwater) + if(mPos.squaredDistance(mOutput.mPos) > mMaxDistance*mMaxDistance) + gain = 0.0f; + else if(!(mFlags&MWBase::SoundManager::Play_NoEnv) && mOutput.mLastEnvironment == Env_Underwater) { gain *= 0.9f; pitch *= 0.7f; @@ -694,7 +696,7 @@ void OpenAL_Output::init(const std::string &devname) fail(std::string("Failed to setup context: ")+alcGetString(mDevice, alcGetError(mDevice))); } - alDistanceModel(AL_INVERSE_DISTANCE); + alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); throwALerror(); ALCint maxmono=0, maxstereo=0; diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index 251b05890b..d856f41ee6 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -385,6 +385,11 @@ namespace MWSound const ESM::Position &pos = ptr.getRefData().getPosition(); const Ogre::Vector3 objpos(pos.pos); + if ((mode & Play_RemoveAtDistance) && mListenerPos.squaredDistance(objpos) > 2000*2000) + { + return MWBase::SoundPtr(); + } + sound = mOutput->playSound3D(file, objpos, volume, basevol, pitch, min, max, mode|type, offset); if((mode&Play_NoTrack)) mActiveSounds[sound] = std::make_pair(MWWorld::Ptr(), soundId); @@ -650,6 +655,13 @@ namespace MWSound const ESM::Position &pos = ptr.getRefData().getPosition(); const Ogre::Vector3 objpos(pos.pos); snditer->first->setPosition(objpos); + + if ((snditer->first->mFlags & Play_RemoveAtDistance) + && mListenerPos.squaredDistance(Ogre::Vector3(ptr.getRefData().getPosition().pos)) > 2000*2000) + { + mActiveSounds.erase(snditer++); + continue; + } } //update fade out if(snditer->first->mFadeOutTime>0) From 0fe7500f7437b34a6100a73adf5877daebd8e1aa Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 9 Dec 2014 00:13:56 +0100 Subject: [PATCH 212/303] Work around pathgrid record limitation (Fixes #2195) --- apps/openmw/mwmechanics/aiwander.cpp | 2 +- apps/openmw/mwmechanics/pathfinding.cpp | 3 +- apps/openmw/mwmechanics/pathgrid.cpp | 9 +-- apps/openmw/mwmechanics/pathgrid.hpp | 2 +- apps/openmw/mwrender/debugging.cpp | 3 +- apps/openmw/mwworld/cellstore.cpp | 2 +- apps/openmw/mwworld/store.hpp | 87 +++++++++---------------- 7 files changed, 41 insertions(+), 67 deletions(-) diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index c8a0c85d58..3224127dfd 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -382,7 +382,7 @@ namespace MWMechanics { // infrequently used, therefore no benefit in caching it as a member const ESM::Pathgrid * - pathgrid = world->getStore().get().search(*cell); + pathgrid = world->getStore().get().search(*cell, world->getCellName(currentCell)); // cache the current cell location cachedCellX = cell->mData.mX; diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index f1279c415e..f0e5b1d9de 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -188,7 +188,8 @@ namespace MWMechanics if(mCell != cell || !mPathgrid) { mCell = cell; - mPathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*mCell->getCell()); + mPathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*mCell->getCell(), + MWBase::Environment::get().getWorld()->getCellName(mCell)); } // Refer to AiWander reseach topic on openmw forums for some background. diff --git a/apps/openmw/mwmechanics/pathgrid.cpp b/apps/openmw/mwmechanics/pathgrid.cpp index 4983a4a4f2..d380cba4e3 100644 --- a/apps/openmw/mwmechanics/pathgrid.cpp +++ b/apps/openmw/mwmechanics/pathgrid.cpp @@ -95,7 +95,7 @@ namespace MWMechanics * +----------------> * high cost */ - bool PathgridGraph::load(const ESM::Cell* cell) + bool PathgridGraph::load(const MWWorld::CellStore *cell) { if(!cell) return false; @@ -103,9 +103,10 @@ namespace MWMechanics if(mIsGraphConstructed) return true; - mCell = cell; - mIsExterior = cell->isExterior(); - mPathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*cell); + mCell = cell->getCell(); + mIsExterior = cell->getCell()->isExterior(); + mPathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*cell->getCell(), + MWBase::Environment::get().getWorld()->getCellName(cell)); if(!mPathgrid) return false; diff --git a/apps/openmw/mwmechanics/pathgrid.hpp b/apps/openmw/mwmechanics/pathgrid.hpp index 5d01dca009..2742957a68 100644 --- a/apps/openmw/mwmechanics/pathgrid.hpp +++ b/apps/openmw/mwmechanics/pathgrid.hpp @@ -21,7 +21,7 @@ namespace MWMechanics public: PathgridGraph(); - bool load(const ESM::Cell *cell); + bool load(const MWWorld::CellStore *cell); // returns true if end point is strongly connected (i.e. reachable // from start point) both start and end are pathgrid point indexes diff --git a/apps/openmw/mwrender/debugging.cpp b/apps/openmw/mwrender/debugging.cpp index 4f5536ca32..553a6379f5 100644 --- a/apps/openmw/mwrender/debugging.cpp +++ b/apps/openmw/mwrender/debugging.cpp @@ -231,8 +231,9 @@ void Debugging::togglePathgrid() void Debugging::enableCellPathgrid(MWWorld::CellStore *store) { + MWBase::World* world = MWBase::Environment::get().getWorld(); const ESM::Pathgrid *pathgrid = - MWBase::Environment::get().getWorld()->getStore().get().search(*store->getCell()); + world->getStore().get().search(*store->getCell(), world->getCellName(store)); if (!pathgrid) return; Vector3 cellPathGridPos(0, 0, 0); diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 52e70fef26..f1a8451ea3 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -413,7 +413,7 @@ namespace MWWorld // TODO: the pathgrid graph only needs to be loaded for active cells, so move this somewhere else. // In a simple test, loading the graph for all cells in MW + expansions took 200 ms - mPathgridGraph.load(mCell); + mPathgridGraph.load(this); } } diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index c8fa087c20..1aaf902f85 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -843,88 +843,59 @@ namespace MWWorld class Store : public StoreBase { private: - typedef std::map Interior; - typedef std::map, ESM::Pathgrid> Exterior; + // 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. + // This is why we keep both interior and exterior pathgrids in the same container here. + typedef std::pair > PathgridKey; + typedef std::map Static; - Interior mInt; - Exterior mExt; + Static mStatic; public: void load(ESM::ESMReader &esm, const std::string &id) { - ESM::Pathgrid pathgrid; pathgrid.load(esm); + PathgridKey key = std::make_pair(pathgrid.mCell, std::make_pair(pathgrid.mData.mX, pathgrid.mData.mY)); + // Try to overwrite existing record - if (!pathgrid.mCell.empty()) - { - 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; - } + std::pair ret = mStatic.insert(std::make_pair(key, pathgrid)); + if (!ret.second) + ret.first->second = pathgrid; } size_t getSize() const { - return mInt.size() + mExt.size(); + return mStatic.size(); } void setUp() { } - const ESM::Pathgrid *search(int x, int y) const { - Exterior::const_iterator it = mExt.find(std::make_pair(x,y)); - if (it != mExt.end()) + const ESM::Pathgrid *search(const ESM::Cell &cell, const std::string& cellName) const { + int x=0,y=0; + if (!(cell.mData.mFlags & ESM::Cell::Interior)) + { + x = cell.mData.mX; + y = cell.mData.mY; + } + PathgridKey key = std::make_pair(cellName, std::make_pair(x,y)); + + Static::const_iterator it = mStatic.find(key); + if (it != mStatic.end()) return &(it->second); return NULL; } - const ESM::Pathgrid *find(int x, int y) const { - const ESM::Pathgrid *ptr = search(x, y); - if (ptr == 0) { + const ESM::Pathgrid *find(const ESM::Cell &cell, const std::string& cellName) const { + const ESM::Pathgrid* pathgrid = search(cell, cellName); + if (pathgrid == 0) { std::ostringstream msg; - msg << "Pathgrid at (" << x << ", " << y << ") not found"; + msg << "Pathgrid in cell '" << cellName << "' not found"; throw std::runtime_error(msg.str()); } - return ptr; - } - - const ESM::Pathgrid *search(const std::string &name) const { - Interior::const_iterator it = mInt.find(name); - if (it != mInt.end()) - return &(it->second); - return NULL; - } - - const ESM::Pathgrid *find(const std::string &name) const { - const ESM::Pathgrid *ptr = search(name); - if (ptr == 0) { - std::ostringstream msg; - msg << "Pathgrid in cell '" << name << "' not found"; - throw std::runtime_error(msg.str()); - } - return ptr; - } - - const ESM::Pathgrid *search(const ESM::Cell &cell) const { - if (cell.mData.mFlags & ESM::Cell::Interior) { - return search(cell.mName); - } - return search(cell.mData.mX, cell.mData.mY); - } - - const ESM::Pathgrid *find(const ESM::Cell &cell) const { - if (cell.mData.mFlags & ESM::Cell::Interior) { - return find(cell.mName); - } - return find(cell.mData.mX, cell.mData.mY); + return pathgrid; } }; From 3ad01899828c42e6c314d291bd7e25cfffeef09f Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 9 Dec 2014 12:06:44 +0100 Subject: [PATCH 213/303] Take sound listener y rotation (roll) into account, though currently unused for actors --- apps/openmw/mwworld/worldimp.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 1b5d6e002f..2607a6d4d0 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1575,7 +1575,8 @@ namespace MWWorld const OEngine::Physic::PhysicActor *actor = mPhysEngine->getCharacter(getPlayerPtr().getRefData().getHandle()); if(actor) playerPos.z += 1.85*actor->getHalfExtents().z; Ogre::Quaternion playerOrient = Ogre::Quaternion(Ogre::Radian(getPlayerPtr().getRefData().getPosition().rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z) * - Ogre::Quaternion(Ogre::Radian(getPlayerPtr().getRefData().getPosition().rot[0]), Ogre::Vector3::NEGATIVE_UNIT_X); + Ogre::Quaternion(Ogre::Radian(getPlayerPtr().getRefData().getPosition().rot[0]), Ogre::Vector3::NEGATIVE_UNIT_X) * + Ogre::Quaternion(Ogre::Radian(getPlayerPtr().getRefData().getPosition().rot[1]), Ogre::Vector3::NEGATIVE_UNIT_Y); MWBase::Environment::get().getSoundManager()->setListenerPosDir(playerPos, playerOrient.yAxis(), playerOrient.zAxis()); } From 109a3f78a1b35990f11c80336242f1b8b9918921 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 9 Dec 2014 16:02:07 +0100 Subject: [PATCH 214/303] Adjust AiFollow distance for groups of multiple followers (Fixes #1637) --- apps/openmw/mwbase/mechanicsmanager.hpp | 1 + apps/openmw/mwmechanics/actors.cpp | 30 ++++++++++++++++ apps/openmw/mwmechanics/actors.hpp | 3 ++ apps/openmw/mwmechanics/aifollow.cpp | 35 +++++++++++++++---- apps/openmw/mwmechanics/aifollow.hpp | 5 +++ .../mwmechanics/mechanicsmanagerimp.cpp | 5 +++ .../mwmechanics/mechanicsmanagerimp.hpp | 1 + 7 files changed, 73 insertions(+), 7 deletions(-) diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index ce213b316a..b7af1cbf72 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -183,6 +183,7 @@ namespace MWBase ///return the list of actors which are following the given actor /**ie AiFollow is active and the target is the actor**/ virtual std::list getActorsFollowing(const MWWorld::Ptr& actor) = 0; + virtual std::list getActorsFollowingIndices(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 **/ diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index a3cfbfd49a..ae430e5c6a 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1470,6 +1470,36 @@ namespace MWMechanics return list; } + std::list Actors::getActorsFollowingIndices(const MWWorld::Ptr &actor) + { + std::list list; + for(PtrControllerMap::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; + + // An actor counts as following if AiFollow is the current AiPackage, or there are only Combat packages before the AiFollow package + for (std::list::const_iterator it = stats.getAiSequence().begin(); it != stats.getAiSequence().end(); ++it) + { + if ((*it)->getTypeId() == MWMechanics::AiPackage::TypeIdFollow) + { + MWWorld::Ptr followTarget = dynamic_cast(*it)->getTarget(); + if (followTarget.isEmpty()) + continue; + if (followTarget == actor) + list.push_back(dynamic_cast(*it)->getFollowIndex()); + else + break; + } + else if ((*it)->getTypeId() != MWMechanics::AiPackage::TypeIdCombat) + break; + } + } + return list; + } + std::list Actors::getActorsFighting(const MWWorld::Ptr& actor) { std::list list; std::vector neighbors; diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 0ccfaad78a..a24095c702 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -112,6 +112,9 @@ namespace MWMechanics /**ie AiFollow is active and the target is the actor **/ std::list getActorsFollowing(const MWWorld::Ptr& actor); + /// Get the list of AiFollow::mFollowIndex for all actors following this target + std::list getActorsFollowingIndices(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); diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index f309dc7402..8f3e19b465 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -6,6 +6,7 @@ #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" #include "creaturestats.hpp" @@ -15,28 +16,31 @@ #include "steering.hpp" +int MWMechanics::AiFollow::mFollowIndexCounter = 0; + MWMechanics::AiFollow::AiFollow(const std::string &actorId,float duration, float x, float y, float z) : mAlwaysFollow(false), mCommanded(false), mRemainingDuration(duration), mX(x), mY(y), mZ(z) -, mActorRefId(actorId), mCellId(""), mActorId(-1) +, mActorRefId(actorId), mCellId(""), mActorId(-1), mFollowIndex(mFollowIndexCounter++) { } MWMechanics::AiFollow::AiFollow(const std::string &actorId,const std::string &cellId,float duration, float x, float y, float z) : mAlwaysFollow(false), mCommanded(false), mRemainingDuration(duration), mX(x), mY(y), mZ(z) -, mActorRefId(actorId), mCellId(cellId), mActorId(-1) +, mActorRefId(actorId), mCellId(cellId), mActorId(-1), mFollowIndex(mFollowIndexCounter++) { } MWMechanics::AiFollow::AiFollow(const std::string &actorId, bool commanded) : mAlwaysFollow(true), mCommanded(commanded), mRemainingDuration(0), mX(0), mY(0), mZ(0) -, mActorRefId(actorId), mCellId(""), mActorId(-1) +, mActorRefId(actorId), mCellId(""), mActorId(-1), mFollowIndex(mFollowIndexCounter++) { + } MWMechanics::AiFollow::AiFollow(const ESM::AiSequence::AiFollow *follow) : mAlwaysFollow(follow->mAlwaysFollow), mRemainingDuration(follow->mRemainingDuration) , mX(follow->mData.mX), mY(follow->mData.mY), mZ(follow->mData.mZ) , mActorRefId(follow->mTargetId), mActorId(-1), mCellId(follow->mCellId) - , mCommanded(follow->mCommanded) + , mCommanded(follow->mCommanded), mFollowIndex(mFollowIndexCounter++) { } @@ -48,12 +52,24 @@ bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor, AiState& state, if (target.isEmpty() || !target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered // with the MechanicsManager ) - return true; //Target doesn't exist + return false; // Target is not here right now, wait for it to return actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); ESM::Position pos = actor.getRefData().getPosition(); //position of the actor + float followDistance = 180; + // When there are multiple actors following the same target, they form a group with each group member at 180*(i+1) distance to the target + int i=0; + std::list followers = MWBase::Environment::get().getMechanicsManager()->getActorsFollowingIndices(target); + followers.sort(); + for (std::list::iterator it = followers.begin(); it != followers.end(); ++it) + { + if (*it == mFollowIndex) + followDistance *= (i+1); + ++i; + } + if(!mAlwaysFollow) //Update if you only follow for a bit { //Check if we've run out of time @@ -66,7 +82,7 @@ bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor, AiState& state, if((pos.pos[0]-mX)*(pos.pos[0]-mX) + (pos.pos[1]-mY)*(pos.pos[1]-mY) + - (pos.pos[2]-mZ)*(pos.pos[2]-mZ) < 100*100) //Close-ish to final position + (pos.pos[2]-mZ)*(pos.pos[2]-mZ) < followDistance*followDistance) //Close-ish to final position { if(actor.getCell()->isExterior()) //Outside? { @@ -84,7 +100,7 @@ bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor, AiState& state, //Set the target destination from the actor ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; - if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < 100) //Stop when you get close + if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < followDistance) //Stop when you get close actor.getClass().getMovementSettings(actor).mPosition[1] = 0; else { pathTo(actor, dest, duration); //Go to the destination @@ -159,3 +175,8 @@ MWWorld::Ptr MWMechanics::AiFollow::getTarget() else return MWWorld::Ptr(); } + +int MWMechanics::AiFollow::getFollowIndex() const +{ + return mFollowIndex; +} diff --git a/apps/openmw/mwmechanics/aifollow.hpp b/apps/openmw/mwmechanics/aifollow.hpp index d5dd42826d..23b159b882 100644 --- a/apps/openmw/mwmechanics/aifollow.hpp +++ b/apps/openmw/mwmechanics/aifollow.hpp @@ -46,6 +46,8 @@ namespace MWMechanics bool isCommanded() const; + int getFollowIndex() const; + private: /// This will make the actor always follow. /** Thus ignoring mDuration and mX,mY,mZ (used for summoned creatures). **/ @@ -58,6 +60,9 @@ namespace MWMechanics std::string mActorRefId; int mActorId; std::string mCellId; + int mFollowIndex; + + static int mFollowIndexCounter; }; } #endif diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 9cafe9b3ce..1dbe6f950e 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -1268,6 +1268,11 @@ namespace MWMechanics return mActors.getActorsFollowing(actor); } + std::list MechanicsManager::getActorsFollowingIndices(const MWWorld::Ptr& actor) + { + return mActors.getActorsFollowingIndices(actor); + } + std::list MechanicsManager::getActorsFighting(const MWWorld::Ptr& actor) { return mActors.getActorsFighting(actor); } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 9f9e85c5af..1eec26c8ac 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -147,6 +147,7 @@ namespace MWMechanics virtual void getActorsInRange(const Ogre::Vector3 &position, float radius, std::vector &objects); virtual std::list getActorsFollowing(const MWWorld::Ptr& actor); + virtual std::list getActorsFollowingIndices(const MWWorld::Ptr& actor); virtual std::list getActorsFighting(const MWWorld::Ptr& actor); From e0c6f845464f211fc7f87a11e5b2b9c9a3b3d554 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 9 Dec 2014 22:25:28 +0100 Subject: [PATCH 215/303] AiFollow: target has to be seen in order to start following (Fixes #1637) --- apps/openmw/mwmechanics/aifollow.cpp | 77 +++++++++++++++++++++------- apps/openmw/mwmechanics/aifollow.hpp | 1 + components/esm/aisequence.cpp | 4 ++ components/esm/aisequence.hpp | 2 + 4 files changed, 66 insertions(+), 18 deletions(-) diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index 8f3e19b465..161f4bb905 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -13,39 +13,51 @@ #include "movement.hpp" #include +#include #include "steering.hpp" -int MWMechanics::AiFollow::mFollowIndexCounter = 0; +namespace MWMechanics +{ -MWMechanics::AiFollow::AiFollow(const std::string &actorId,float duration, float x, float y, float z) + +struct AiFollowStorage : AiTemporaryBase +{ + float mTimer; + + AiFollowStorage() : mTimer(0.f) {} +}; + +int AiFollow::mFollowIndexCounter = 0; + +AiFollow::AiFollow(const std::string &actorId,float duration, float x, float y, float z) : mAlwaysFollow(false), mCommanded(false), mRemainingDuration(duration), mX(x), mY(y), mZ(z) -, mActorRefId(actorId), mCellId(""), mActorId(-1), mFollowIndex(mFollowIndexCounter++) +, mActorRefId(actorId), mCellId(""), mActorId(-1), mFollowIndex(mFollowIndexCounter++), mActive(false) { } -MWMechanics::AiFollow::AiFollow(const std::string &actorId,const std::string &cellId,float duration, float x, float y, float z) +AiFollow::AiFollow(const std::string &actorId,const std::string &cellId,float duration, float x, float y, float z) : mAlwaysFollow(false), mCommanded(false), mRemainingDuration(duration), mX(x), mY(y), mZ(z) -, mActorRefId(actorId), mCellId(cellId), mActorId(-1), mFollowIndex(mFollowIndexCounter++) +, mActorRefId(actorId), mCellId(cellId), mActorId(-1), mFollowIndex(mFollowIndexCounter++), mActive(false) { } -MWMechanics::AiFollow::AiFollow(const std::string &actorId, bool commanded) +AiFollow::AiFollow(const std::string &actorId, bool commanded) : mAlwaysFollow(true), mCommanded(commanded), mRemainingDuration(0), mX(0), mY(0), mZ(0) -, mActorRefId(actorId), mCellId(""), mActorId(-1), mFollowIndex(mFollowIndexCounter++) +, mActorRefId(actorId), mCellId(""), mActorId(-1), mFollowIndex(mFollowIndexCounter++), mActive(false) { } -MWMechanics::AiFollow::AiFollow(const ESM::AiSequence::AiFollow *follow) +AiFollow::AiFollow(const ESM::AiSequence::AiFollow *follow) : mAlwaysFollow(follow->mAlwaysFollow), mRemainingDuration(follow->mRemainingDuration) , mX(follow->mData.mX), mY(follow->mData.mY), mZ(follow->mData.mZ) , mActorRefId(follow->mTargetId), mActorId(-1), mCellId(follow->mCellId) - , mCommanded(follow->mCommanded), mFollowIndex(mFollowIndexCounter++) + , mCommanded(follow->mCommanded), mFollowIndex(mFollowIndexCounter++), mActive(follow->mActive) { } -bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor, AiState& state, float duration) +bool AiFollow::execute (const MWWorld::Ptr& actor, AiState& state, float duration) { MWWorld::Ptr target = getTarget(); @@ -56,6 +68,24 @@ bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor, AiState& state, actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + // AiFollow requires the target to be in range and within sight for the initial activation + if (!mActive) + { + AiFollowStorage& storage = state.get(); + storage.mTimer -= duration; + + if (storage.mTimer < 0) + { + if (Ogre::Vector3(actor.getRefData().getPosition().pos).squaredDistance(Ogre::Vector3(target.getRefData().getPosition().pos)) + < 500*500 + && MWBase::Environment::get().getWorld()->getLOS(actor, target)) + mActive = true; + storage.mTimer = 0.5f; + } + } + if (!mActive) + return false; + ESM::Position pos = actor.getRefData().getPosition(); //position of the actor float followDistance = 180; @@ -101,8 +131,16 @@ bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor, AiState& state, ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < followDistance) //Stop when you get close + { actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - else { + + // turn towards target anyway + float directionX = target.getRefData().getPosition().pos[0] - actor.getRefData().getPosition().pos[0]; + float directionY = target.getRefData().getPosition().pos[1] - actor.getRefData().getPosition().pos[1]; + zTurn(actor, Ogre::Math::ATan2(directionX,directionY), Ogre::Degree(5)); + } + else + { pathTo(actor, dest, duration); //Go to the destination } @@ -115,27 +153,27 @@ bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor, AiState& state, return false; } -std::string MWMechanics::AiFollow::getFollowedActor() +std::string AiFollow::getFollowedActor() { return mActorRefId; } -MWMechanics::AiFollow *MWMechanics::AiFollow::clone() const +AiFollow *MWMechanics::AiFollow::clone() const { return new AiFollow(*this); } -int MWMechanics::AiFollow::getTypeId() const +int AiFollow::getTypeId() const { return TypeIdFollow; } -bool MWMechanics::AiFollow::isCommanded() const +bool AiFollow::isCommanded() const { return mCommanded; } -void MWMechanics::AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const +void AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const { std::auto_ptr follow(new ESM::AiSequence::AiFollow()); follow->mData.mX = mX; @@ -146,6 +184,7 @@ void MWMechanics::AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) co follow->mCellId = mCellId; follow->mAlwaysFollow = mAlwaysFollow; follow->mCommanded = mCommanded; + follow->mActive = mActive; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Follow; @@ -153,7 +192,7 @@ void MWMechanics::AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) co sequence.mPackages.push_back(package); } -MWWorld::Ptr MWMechanics::AiFollow::getTarget() +MWWorld::Ptr AiFollow::getTarget() { if (mActorId == -2) return MWWorld::Ptr(); @@ -176,7 +215,9 @@ MWWorld::Ptr MWMechanics::AiFollow::getTarget() return MWWorld::Ptr(); } -int MWMechanics::AiFollow::getFollowIndex() const +int AiFollow::getFollowIndex() const { return mFollowIndex; } + +} diff --git a/apps/openmw/mwmechanics/aifollow.hpp b/apps/openmw/mwmechanics/aifollow.hpp index 23b159b882..68a1f0ea5f 100644 --- a/apps/openmw/mwmechanics/aifollow.hpp +++ b/apps/openmw/mwmechanics/aifollow.hpp @@ -60,6 +60,7 @@ namespace MWMechanics std::string mActorRefId; int mActorId; std::string mCellId; + bool mActive; // have we spotted the target? int mFollowIndex; static int mFollowIndexCounter; diff --git a/components/esm/aisequence.cpp b/components/esm/aisequence.cpp index 0e3e541024..339b390d74 100644 --- a/components/esm/aisequence.cpp +++ b/components/esm/aisequence.cpp @@ -60,6 +60,8 @@ namespace AiSequence esm.getHNT (mAlwaysFollow, "ALWY"); mCommanded = false; esm.getHNOT (mCommanded, "CMND"); + mActive = false; + esm.getHNOT (mActive, "ACTV"); } void AiFollow::save(ESMWriter &esm) const @@ -71,6 +73,8 @@ namespace AiSequence esm.writeHNString ("CELL", mCellId); esm.writeHNT ("ALWY", mAlwaysFollow); esm.writeHNT ("CMND", mCommanded); + if (mActive) + esm.writeHNT("ACTV", mActive); } void AiActivate::load(ESMReader &esm) diff --git a/components/esm/aisequence.hpp b/components/esm/aisequence.hpp index da16bf867a..fbf83c2455 100644 --- a/components/esm/aisequence.hpp +++ b/components/esm/aisequence.hpp @@ -98,6 +98,8 @@ namespace ESM bool mAlwaysFollow; bool mCommanded; + bool mActive; + void load(ESMReader &esm); void save(ESMWriter &esm) const; }; From 3c747195ae2f8bcc82d2c978ecdf845cafd10b03 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 10 Dec 2014 17:21:34 +0100 Subject: [PATCH 216/303] Add fall damage for creatures (Fixes #2201) --- apps/openmw/mwclass/npc.cpp | 31 --------------------------- apps/openmw/mwclass/npc.hpp | 3 --- apps/openmw/mwmechanics/character.cpp | 31 ++++++++++++++++++++++++++- apps/openmw/mwworld/class.cpp | 5 ----- apps/openmw/mwworld/class.hpp | 3 --- 5 files changed, 30 insertions(+), 43 deletions(-) diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 641edcc834..49f76d25c6 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -1002,37 +1002,6 @@ namespace MWClass return x; } - float Npc::getFallDamage(const MWWorld::Ptr &ptr, float fallHeight) const - { - MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &store = world->getStore().get(); - - const float fallDistanceMin = store.find("fFallDamageDistanceMin")->getFloat(); - - if (fallHeight >= fallDistanceMin) - { - const float acrobaticsSkill = ptr.getClass().getNpcStats (ptr).getSkill(ESM::Skill::Acrobatics).getModified(); - const NpcCustomData *npcdata = static_cast(ptr.getRefData().getCustomData()); - const float jumpSpellBonus = npcdata->mNpcStats.getMagicEffects().get(ESM::MagicEffect::Jump).getMagnitude(); - const float fallAcroBase = store.find("fFallAcroBase")->getFloat(); - const float fallAcroMult = store.find("fFallAcroMult")->getFloat(); - const float fallDistanceBase = store.find("fFallDistanceBase")->getFloat(); - const float fallDistanceMult = store.find("fFallDistanceMult")->getFloat(); - - float x = fallHeight - fallDistanceMin; - x -= (1.5 * acrobaticsSkill) + jumpSpellBonus; - x = std::max(0.0f, x); - - float a = fallAcroBase + fallAcroMult * (100 - acrobaticsSkill); - x = fallDistanceBase + fallDistanceMult * x; - x *= a; - - return x; - } - - return 0; - } - MWMechanics::Movement& Npc::getMovementSettings (const MWWorld::Ptr& ptr) const { ensureCustomData (ptr); diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp index fd16e6f083..a92e72af59 100644 --- a/apps/openmw/mwclass/npc.hpp +++ b/apps/openmw/mwclass/npc.hpp @@ -104,9 +104,6 @@ namespace MWClass virtual float getJump(const MWWorld::Ptr &ptr) const; ///< Return jump velocity (not accounting for movement) - virtual float getFallDamage(const MWWorld::Ptr &ptr, float fallHeight) const; - ///< Return amount of health points lost when falling - virtual MWMechanics::Movement& getMovementSettings (const MWWorld::Ptr& ptr) const; ///< Return desired movement. diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 46e06f4604..87d9a5b679 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -92,6 +92,35 @@ MWMechanics::CharacterState runStateToWalkState (MWMechanics::CharacterState sta 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")->getFloat(); + + if (fallHeight >= fallDistanceMin) + { + const float acrobaticsSkill = 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")->getFloat(); + const float fallAcroMult = store.find("fFallAcroMult")->getFloat(); + const float fallDistanceBase = store.find("fFallDistanceBase")->getFloat(); + const float fallDistanceMult = store.find("fFallDistanceMult")->getFloat(); + + float x = fallHeight - fallDistanceMin; + x -= (1.5 * 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 @@ -1449,7 +1478,7 @@ void CharacterController::update(float duration) vec.z = 0.0f; float height = cls.getCreatureStats(mPtr).land(); - float healthLost = cls.getFallDamage(mPtr, height); + float healthLost = getFallDamage(mPtr, height); if (healthLost > 0.0f) { const float fatigueTerm = cls.getCreatureStats(mPtr).getFatigueTerm(); diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index 16c2469c14..0a84862097 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -180,11 +180,6 @@ namespace MWWorld throw std::runtime_error ("class does not support enchanting"); } - float Class::getFallDamage(const MWWorld::Ptr &ptr, float fallHeight) const - { - return 0; - } - MWMechanics::Movement& Class::getMovementSettings (const Ptr& ptr) const { throw std::runtime_error ("movement settings not supported by class"); diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index b66ca74880..dcac16b06e 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -184,9 +184,6 @@ namespace MWWorld virtual float getJump(const MWWorld::Ptr &ptr) const; ///< Return jump velocity (not accounting for movement) - virtual float getFallDamage(const MWWorld::Ptr &ptr, float fallHeight) const; - ///< Return amount of health points lost when falling - virtual MWMechanics::Movement& getMovementSettings (const Ptr& ptr) const; ///< Return desired movement. From ead6bf16011bca8f4b6d35a01b1a6a2a68486d55 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 10 Dec 2014 17:30:33 +0100 Subject: [PATCH 217/303] Enchanting: cast the enchant points for the item to int (Fixes #2202) --- apps/openmw/mwmechanics/enchanting.cpp | 6 +++--- apps/openmw/mwmechanics/enchanting.hpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp index c134942b88..759b2a7bba 100644 --- a/apps/openmw/mwmechanics/enchanting.cpp +++ b/apps/openmw/mwmechanics/enchanting.cpp @@ -156,7 +156,7 @@ namespace MWMechanics * * Formula on UESPWiki is not entirely correct. */ - float Enchanting::getEnchantPoints() const + int Enchanting::getEnchantPoints() const { if (mEffectList.mList.empty()) // No effects added, cost = 0 @@ -195,7 +195,7 @@ namespace MWMechanics --effectsLeftCnt; } - return enchantmentCost; + return static_cast(enchantmentCost); } @@ -240,7 +240,7 @@ namespace MWMechanics return soul->mData.mSoul; } - float Enchanting::getMaxEnchantValue() const + int Enchanting::getMaxEnchantValue() const { if (itemEmpty()) return 0; diff --git a/apps/openmw/mwmechanics/enchanting.hpp b/apps/openmw/mwmechanics/enchanting.hpp index 01ca1e0e1d..d41305c4ac 100644 --- a/apps/openmw/mwmechanics/enchanting.hpp +++ b/apps/openmw/mwmechanics/enchanting.hpp @@ -35,10 +35,10 @@ namespace MWMechanics 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() const; + int getEnchantPoints() const; float getCastCost() const; int getEnchantPrice() const; - float getMaxEnchantValue() const; + int getMaxEnchantValue() const; int getGemCharge() const; float getEnchantChance() const; bool soulEmpty() const; //Return true if empty From 74c345f79057504ed0976648b82954aa21fa95ee Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 10 Dec 2014 17:40:59 +0100 Subject: [PATCH 218/303] Enchanting: fix being able to create On Touch / On Target constant effect enchantments (this combination makes no sense) --- apps/openmw/mwgui/spellcreationdialog.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 00cab6c455..2124c27247 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -198,11 +198,11 @@ namespace MWGui mRangeButton->setCaptionWithReplacing ("#{sRangeTouch}"); // cycle through range types until we find something that's allowed - if (mEffect.mRange == ESM::RT_Target && !(mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTarget)) + if (mEffect.mRange == ESM::RT_Target && (!(mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTarget) || constantEffect)) onRangeButtonClicked(sender); if (mEffect.mRange == ESM::RT_Self && !(mMagicEffect->mData.mFlags & ESM::MagicEffect::CastSelf)) onRangeButtonClicked(sender); - if (mEffect.mRange == ESM::RT_Touch && !(mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTouch)) + if (mEffect.mRange == ESM::RT_Touch && (!(mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTouch) || constantEffect)) onRangeButtonClicked(sender); if(mEffect.mRange == ESM::RT_Self) From 619ea846b47ad4e45b6a4e90981810a6e9c3ff00 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 10 Dec 2014 17:48:49 +0100 Subject: [PATCH 219/303] Enchanting: fixed case where no range types at all are allowed (e.g. a Constant Effect item with an effect that does not allow the Self range-type) --- apps/openmw/mwgui/spellcreationdialog.cpp | 47 ++++++++++++++--------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 2124c27247..5da33c67ae 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -89,15 +89,22 @@ namespace MWGui void EditEffectDialog::newEffect (const ESM::MagicEffect *effect) { + bool allowSelf = effect->mData.mFlags & ESM::MagicEffect::CastSelf; + bool allowTouch = (effect->mData.mFlags & ESM::MagicEffect::CastTouch) && !constantEffect; + bool allowTarget = (effect->mData.mFlags & ESM::MagicEffect::CastTarget) && !constantEffect; + + if (!allowSelf && !allowTouch && !allowTarget) + return; // TODO: Show an error message popup? + setMagicEffect(effect); mEditing = false; mDeleteButton->setVisible (false); mEffect.mRange = ESM::RT_Self; - if (!(mMagicEffect->mData.mFlags & ESM::MagicEffect::CastSelf)) + if (!allowSelf) mEffect.mRange = ESM::RT_Touch; - if (!(mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTouch)) + if (!allowTouch) mEffect.mRange = ESM::RT_Target; mEffect.mMagnMin = 1; mEffect.mMagnMax = 1; @@ -118,6 +125,8 @@ namespace MWGui mMagnitudeMinValue->setCaption("1"); mMagnitudeMaxValue->setCaption("- 1"); mAreaValue->setCaption("0"); + + setVisible(true); } void EditEffectDialog::editEffect (ESM::ENAMstruct effect) @@ -190,6 +199,24 @@ namespace MWGui { 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; + bool allowTouch = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTouch) && !constantEffect; + bool allowTarget = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTarget) && !constantEffect; + 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) @@ -197,19 +224,6 @@ namespace MWGui else if (mEffect.mRange == ESM::RT_Touch) mRangeButton->setCaptionWithReplacing ("#{sRangeTouch}"); - // cycle through range types until we find something that's allowed - if (mEffect.mRange == ESM::RT_Target && (!(mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTarget) || constantEffect)) - onRangeButtonClicked(sender); - if (mEffect.mRange == ESM::RT_Self && !(mMagicEffect->mData.mFlags & ESM::MagicEffect::CastSelf)) - onRangeButtonClicked(sender); - if (mEffect.mRange == ESM::RT_Touch && (!(mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTouch) || constantEffect)) - onRangeButtonClicked(sender); - - if(mEffect.mRange == ESM::RT_Self) - { - mAreaSlider->setScrollPosition(0); - onAreaChanged(mAreaSlider,0); - } updateBoxes(); eventEffectModified(mEffect); } @@ -542,7 +556,6 @@ namespace MWGui mAddEffectDialog.newEffect(effect); mAddEffectDialog.setAttribute (mSelectAttributeDialog->getAttributeId()); - mAddEffectDialog.setVisible(true); MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectAttributeDialog); mSelectAttributeDialog = 0; } @@ -554,7 +567,6 @@ namespace MWGui mAddEffectDialog.newEffect(effect); mAddEffectDialog.setSkill (mSelectSkillDialog->getSkillId()); - mAddEffectDialog.setVisible(true); MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectSkillDialog); mSelectSkillDialog = 0; } @@ -611,7 +623,6 @@ namespace MWGui else { mAddEffectDialog.newEffect(effect); - mAddEffectDialog.setVisible(true); } } From 623783cd6ac77da581a8106aee5abfa02bec2feb Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 10 Dec 2014 18:05:11 +0100 Subject: [PATCH 220/303] Enchanting: fix cast cost for "on use" enchantments being set incorrectly --- apps/openmw/mwgui/enchantingdialog.cpp | 4 ++-- apps/openmw/mwmechanics/enchanting.cpp | 6 +++--- apps/openmw/mwmechanics/enchanting.hpp | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/openmw/mwgui/enchantingdialog.cpp b/apps/openmw/mwgui/enchantingdialog.cpp index 30d67f5540..56caa6513b 100644 --- a/apps/openmw/mwgui/enchantingdialog.cpp +++ b/apps/openmw/mwgui/enchantingdialog.cpp @@ -109,8 +109,8 @@ namespace MWGui mCharge->setCaption(boost::lexical_cast(mEnchanting.getGemCharge())); std::stringstream castCost; - castCost << std::setprecision(1) << std::fixed << mEnchanting.getCastCost(); - mCastCost->setCaption(boost::lexical_cast(castCost.str())); + castCost << mEnchanting.getCastCost(); + mCastCost->setCaption(castCost.str()); mPrice->setCaption(boost::lexical_cast(mEnchanting.getEnchantPrice())); diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp index 759b2a7bba..8c85e5eef5 100644 --- a/apps/openmw/mwmechanics/enchanting.cpp +++ b/apps/openmw/mwmechanics/enchanting.cpp @@ -55,7 +55,7 @@ namespace MWMechanics enchantment.mData.mCharge = getGemCharge(); enchantment.mData.mAutocalc = 0; enchantment.mData.mType = mCastStyle; - enchantment.mData.mCost = getEnchantPoints(); + enchantment.mData.mCost = getCastCost(); store.remove(mSoulGemPtr, 1, player); @@ -199,7 +199,7 @@ namespace MWMechanics } - float Enchanting::getCastCost() const + int Enchanting::getCastCost() const { if (mCastStyle == ESM::Enchantment::ConstantEffect) return 0; @@ -215,7 +215,7 @@ namespace MWMechanics */ const float castCost = enchantCost - (enchantCost / 100) * (eSkill - 10); - return (castCost < 1) ? 1 : castCost; + return static_cast((castCost < 1) ? 1 : castCost); } diff --git a/apps/openmw/mwmechanics/enchanting.hpp b/apps/openmw/mwmechanics/enchanting.hpp index d41305c4ac..2ee5ccce4e 100644 --- a/apps/openmw/mwmechanics/enchanting.hpp +++ b/apps/openmw/mwmechanics/enchanting.hpp @@ -36,7 +36,7 @@ namespace MWMechanics void nextCastStyle(); //Set enchant type to next possible type (for mOldItemPtr object) int getCastStyle() const; int getEnchantPoints() const; - float getCastCost() const; + int getCastCost() const; int getEnchantPrice() const; int getMaxEnchantValue() const; int getGemCharge() const; From 6eebe9b44c2bb041b05854e70b27b4340a608181 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 10 Dec 2014 20:51:32 +0100 Subject: [PATCH 221/303] Read NiFogProperty, but don't use it yet (Feature #920) --- components/nif/niffile.cpp | 1 + components/nif/property.hpp | 16 ++++++++++++++++ components/nif/record.hpp | 1 + 3 files changed, 18 insertions(+) diff --git a/components/nif/niffile.cpp b/components/nif/niffile.cpp index c689e27b3a..9d63ac7ab5 100644 --- a/components/nif/niffile.cpp +++ b/components/nif/niffile.cpp @@ -57,6 +57,7 @@ static std::map makeFactory() newFactory.insert(makeEntry("NiCamera", &construct , RC_NiCamera )); newFactory.insert(makeEntry("RootCollisionNode", &construct , RC_RootCollisionNode )); newFactory.insert(makeEntry("NiTexturingProperty", &construct , RC_NiTexturingProperty )); + newFactory.insert(makeEntry("NiFogProperty", &construct , RC_NiFogProperty )); newFactory.insert(makeEntry("NiMaterialProperty", &construct , RC_NiMaterialProperty )); newFactory.insert(makeEntry("NiZBufferProperty", &construct , RC_NiZBufferProperty )); newFactory.insert(makeEntry("NiAlphaProperty", &construct , RC_NiAlphaProperty )); diff --git a/components/nif/property.hpp b/components/nif/property.hpp index 2c7747a3ec..77f61d0684 100644 --- a/components/nif/property.hpp +++ b/components/nif/property.hpp @@ -155,6 +155,22 @@ public: } }; +class NiFogProperty : public Property +{ +public: + float mFogDepth; + Ogre::Vector3 mColour; + + + void read(NIFStream *nif) + { + Property::read(nif); + + mFogDepth = nif->getFloat(); + mColour = nif->getVector3(); + } +}; + // These contain no other data than the 'flags' field in Property class NiShadeProperty : public Property { }; class NiDitherProperty : public Property { }; diff --git a/components/nif/record.hpp b/components/nif/record.hpp index 079b335f05..07d7540f85 100644 --- a/components/nif/record.hpp +++ b/components/nif/record.hpp @@ -44,6 +44,7 @@ enum RecordType RC_NiBSParticleNode, RC_NiCamera, RC_NiTexturingProperty, + RC_NiFogProperty, RC_NiMaterialProperty, RC_NiZBufferProperty, RC_NiAlphaProperty, From 33019b93b4a17c7facb077abb9836efcfa1409b7 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 10 Dec 2014 21:10:14 +0100 Subject: [PATCH 222/303] Fix bug setting current launcher profile on startup (Bug #2188) --- apps/launcher/datafilespage.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index f45b444707..4015579c20 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -238,10 +238,8 @@ void Launcher::DataFilesPage::addProfile (const QString &profile, bool setAsCurr if (profile.isEmpty()) return; - if (ui.profilesComboBox->findText (profile) != -1) - return; - - ui.profilesComboBox->addItem (profile); + if (ui.profilesComboBox->findText (profile) == -1) + ui.profilesComboBox->addItem (profile); if (setAsCurrent) setProfile (ui.profilesComboBox->findText (profile), false); From fb1aa096beb67042d3bcb997fead1be4a9b9e1b6 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 10 Dec 2014 21:46:36 +0100 Subject: [PATCH 223/303] Settings: reduce scope for better readability --- components/config/settingsbase.hpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/components/config/settingsbase.hpp b/components/config/settingsbase.hpp index 92ca34cdfe..e6b0908e0a 100644 --- a/components/config/settingsbase.hpp +++ b/components/config/settingsbase.hpp @@ -50,7 +50,7 @@ namespace Config bool readFile(QTextStream &stream) { - mCache.clear(); + Map cache; QString sectionPrefix; @@ -79,31 +79,30 @@ namespace Config mSettings.remove(key); - QStringList values = mCache.values(key); + QStringList values = cache.values(key); if (!values.contains(value)) { if (mMultiValue) { - mCache.insertMulti(key, value); + cache.insertMulti(key, value); } else { - mCache.insert(key, value); + cache.insert(key, value); } } } } if (mSettings.isEmpty()) { - mSettings = mCache; // This is the first time we read a file + mSettings = cache; // This is the first time we read a file return true; } // Merge the changed keys with those which didn't - mSettings.unite(mCache); + mSettings.unite(cache); return true; } private: Map mSettings; - Map mCache; bool mMultiValue; }; From 1937ace1b748e66b9044e679fd3923172601c006 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 10 Dec 2014 21:47:04 +0100 Subject: [PATCH 224/303] Launcher: fix bugs deleting profiles (Fixes #2188) --- apps/launcher/datafilespage.cpp | 18 ++++++++++-------- apps/launcher/datafilespage.hpp | 2 ++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 4015579c20..7192ed7842 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -154,9 +154,11 @@ void Launcher::DataFilesPage::setProfile(int index, bool savePrevious) { if (index >= -1 && index < ui.profilesComboBox->count()) { - QString previous = ui.profilesComboBox->itemText(ui.profilesComboBox->currentIndex()); + QString previous = mPreviousProfile; QString current = ui.profilesComboBox->itemText(index); + mPreviousProfile = current; + setProfile (previous, current, savePrevious); } } @@ -167,9 +169,6 @@ void Launcher::DataFilesPage::setProfile (const QString &previous, const QString if (previous == current) return; - if (previous.isEmpty()) - return; - if (!previous.isEmpty() && savePrevious) saveSettings (previous); @@ -212,7 +211,7 @@ void Launcher::DataFilesPage::slotProfileChanged(int index) void Launcher::DataFilesPage::on_newProfileAction_triggered() { - if (!mProfileDialog->exec() == QDialog::Accepted) + if (mProfileDialog->exec() != QDialog::Accepted) return; QString profile = mProfileDialog->lineEdit()->text(); @@ -222,9 +221,10 @@ void Launcher::DataFilesPage::on_newProfileAction_triggered() saveSettings(); - mSelector->clearCheckStates(); + mLauncherSettings.setValue(QString("Profiles/currentprofile"), profile); addProfile(profile, true); + mSelector->clearCheckStates(); mSelector->setGameFile(); @@ -255,10 +255,12 @@ void Launcher::DataFilesPage::on_deleteProfileAction_triggered() if (!showDeleteMessageBox (profile)) return; - // Remove the profile from the combobox - ui.profilesComboBox->removeItem (ui.profilesComboBox->findText (profile)); + // this should work since the Default profile can't be deleted and is always index 0 + int next = ui.profilesComboBox->currentIndex()-1; + ui.profilesComboBox->setCurrentIndex(next); removeProfile(profile); + ui.profilesComboBox->removeItem(ui.profilesComboBox->findText(profile)); saveSettings(); diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index 5beeb0e03a..15fa00308d 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -67,6 +67,8 @@ namespace Launcher Config::GameSettings &mGameSettings; Config::LauncherSettings &mLauncherSettings; + QString mPreviousProfile; + QString mDataLocal; void setPluginsCheckstates(Qt::CheckState state); From 04a68fc9765eb57c613f0054c9d8f5198d2699bd Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 11 Dec 2014 00:20:45 +0100 Subject: [PATCH 225/303] Revert "Implement overwriting pathgrid records" This broke pathgrid loading in exterior cells due to an unexpected problem in the pathgrid record system (bug #2195). This reverts commit dd0cea21b0ead17a13198f3e584d343c6bb31875. --- apps/openmw/mwworld/store.hpp | 32 ++------------------------------ 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index 469b93f88e..55c5b8191f 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -876,36 +876,8 @@ namespace MWWorld public: void load(ESM::ESMReader &esm, const std::string &id) { - - ESM::Pathgrid pathgrid; - pathgrid.load(esm); - - // Try to overwrite existing record - // Can't use search() because we aren't sorted yet - if (!pathgrid.mCell.empty()) - { - for (std::vector::iterator it = mStatic.begin(); it != mStatic.end(); ++it) - { - if ((*it).mCell == pathgrid.mCell) - { - (*it) = pathgrid; - return; - } - } - } - else - { - for (std::vector::iterator it = mStatic.begin(); it != mStatic.end(); ++it) - { - if ((*it).mData.mX == pathgrid.mData.mX && (*it).mData.mY == pathgrid.mData.mY) - { - (*it) = pathgrid; - return; - } - } - } - - mStatic.push_back(pathgrid); + mStatic.push_back(ESM::Pathgrid()); + mStatic.back().load(esm); } size_t getSize() const { From 46cf2a7a774fd96bd15f2f22c48d23dbba332e7d Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 11 Dec 2014 09:20:18 +0100 Subject: [PATCH 226/303] updated changelog --- readme.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/readme.txt b/readme.txt index e91455aebe..83f139a1d8 100644 --- a/readme.txt +++ b/readme.txt @@ -160,7 +160,6 @@ Bug #2161: Editor: combat/magic/stealth values of creature not displayed correct 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 #2175: Pathgrid mods do not overwrite the existing pathgrid Bug #2180: Editor: Verifier doesn't handle Windows-specific path issues when dealing with resources Feature #238: Add UI to run INI-importer from the launcher Feature #854: Editor: Add user setting to show status bar From 9ef6e95bf6a1677f47ca0717a711ab0390bf057d Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 10 Dec 2014 21:47:04 +0100 Subject: [PATCH 227/303] Launcher: fix bugs deleting profiles (Fixes #2188) --- apps/launcher/datafilespage.cpp | 18 ++++++++++-------- apps/launcher/datafilespage.hpp | 2 ++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index f45b444707..063d601975 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -154,9 +154,11 @@ void Launcher::DataFilesPage::setProfile(int index, bool savePrevious) { if (index >= -1 && index < ui.profilesComboBox->count()) { - QString previous = ui.profilesComboBox->itemText(ui.profilesComboBox->currentIndex()); + QString previous = mPreviousProfile; QString current = ui.profilesComboBox->itemText(index); + mPreviousProfile = current; + setProfile (previous, current, savePrevious); } } @@ -167,9 +169,6 @@ void Launcher::DataFilesPage::setProfile (const QString &previous, const QString if (previous == current) return; - if (previous.isEmpty()) - return; - if (!previous.isEmpty() && savePrevious) saveSettings (previous); @@ -212,7 +211,7 @@ void Launcher::DataFilesPage::slotProfileChanged(int index) void Launcher::DataFilesPage::on_newProfileAction_triggered() { - if (!mProfileDialog->exec() == QDialog::Accepted) + if (mProfileDialog->exec() != QDialog::Accepted) return; QString profile = mProfileDialog->lineEdit()->text(); @@ -222,9 +221,10 @@ void Launcher::DataFilesPage::on_newProfileAction_triggered() saveSettings(); - mSelector->clearCheckStates(); + mLauncherSettings.setValue(QString("Profiles/currentprofile"), profile); addProfile(profile, true); + mSelector->clearCheckStates(); mSelector->setGameFile(); @@ -257,10 +257,12 @@ void Launcher::DataFilesPage::on_deleteProfileAction_triggered() if (!showDeleteMessageBox (profile)) return; - // Remove the profile from the combobox - ui.profilesComboBox->removeItem (ui.profilesComboBox->findText (profile)); + // this should work since the Default profile can't be deleted and is always index 0 + int next = ui.profilesComboBox->currentIndex()-1; + ui.profilesComboBox->setCurrentIndex(next); removeProfile(profile); + ui.profilesComboBox->removeItem(ui.profilesComboBox->findText(profile)); saveSettings(); diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index 5beeb0e03a..15fa00308d 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -67,6 +67,8 @@ namespace Launcher Config::GameSettings &mGameSettings; Config::LauncherSettings &mLauncherSettings; + QString mPreviousProfile; + QString mDataLocal; void setPluginsCheckstates(Qt::CheckState state); From ed66bbb28d4b1dfb2bd00579c2c75aae5ad1b27e Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 10 Dec 2014 21:10:14 +0100 Subject: [PATCH 228/303] Fix bug setting current launcher profile on startup (Bug #2188) --- apps/launcher/datafilespage.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 063d601975..7192ed7842 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -238,10 +238,8 @@ void Launcher::DataFilesPage::addProfile (const QString &profile, bool setAsCurr if (profile.isEmpty()) return; - if (ui.profilesComboBox->findText (profile) != -1) - return; - - ui.profilesComboBox->addItem (profile); + if (ui.profilesComboBox->findText (profile) == -1) + ui.profilesComboBox->addItem (profile); if (setAsCurrent) setProfile (ui.profilesComboBox->findText (profile), false); From cda0363f29f9fa24e603ca0c375e8f8a0e365ba4 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 11 Dec 2014 13:51:37 +0100 Subject: [PATCH 229/303] allow a space in the middle of multi-character comparison operators (Fixes #2185) --- components/compiler/scanner.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/components/compiler/scanner.cpp b/components/compiler/scanner.cpp index 16d54ff51d..203f27e6e8 100644 --- a/components/compiler/scanner.cpp +++ b/components/compiler/scanner.cpp @@ -391,6 +391,10 @@ namespace Compiler { if (get (c)) { + /// \todo hack to allow a space in comparison operators (add option to disable) + if (c==' ') + get (c); + if (c=='=') special = S_cmpEQ; else @@ -398,7 +402,7 @@ namespace Compiler special = S_cmpEQ; putback (c); // return false; -// Allow = as synonym for ==. \todo optionally disable for post-1.0 scripting improvements. +/// Allow = as synonym for ==. \todo optionally disable for post-1.0 scripting improvements. } } else @@ -411,6 +415,10 @@ namespace Compiler { if (get (c)) { + /// \todo hack to allow a space in comparison operators (add option to disable) + if (c==' ' && !get (c)) + return false; + if (c=='=') special = S_cmpNE; else @@ -441,6 +449,10 @@ namespace Compiler { if (get (c)) { + /// \todo hack to allow a space in comparison operators (add option to disable) + if (c==' ') + get (c); + if (c=='=') { special = S_cmpLE; @@ -461,6 +473,10 @@ namespace Compiler { if (get (c)) { + /// \todo hack to allow a space in comparison operators (add option to disable) + if (c==' ') + get (c); + if (c=='=') { special = S_cmpGE; From 3270f0e932de557a9e5ec72390e180ad53477de6 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 11 Dec 2014 15:19:48 +0100 Subject: [PATCH 230/303] Change pathgrid workaround to check for interior cell name --- apps/openmw/mwmechanics/aiwander.cpp | 2 +- apps/openmw/mwmechanics/pathfinding.cpp | 3 +- apps/openmw/mwmechanics/pathgrid.cpp | 4 +- apps/openmw/mwrender/debugging.cpp | 2 +- apps/openmw/mwworld/esmstore.hpp | 2 + apps/openmw/mwworld/store.hpp | 97 ++++++++++++++++++------- 6 files changed, 76 insertions(+), 34 deletions(-) diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 3224127dfd..c8a0c85d58 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -382,7 +382,7 @@ namespace MWMechanics { // infrequently used, therefore no benefit in caching it as a member const ESM::Pathgrid * - pathgrid = world->getStore().get().search(*cell, world->getCellName(currentCell)); + pathgrid = world->getStore().get().search(*cell); // cache the current cell location cachedCellX = cell->mData.mX; diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index f0e5b1d9de..f1279c415e 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -188,8 +188,7 @@ namespace MWMechanics if(mCell != cell || !mPathgrid) { mCell = cell; - mPathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*mCell->getCell(), - MWBase::Environment::get().getWorld()->getCellName(mCell)); + mPathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*mCell->getCell()); } // Refer to AiWander reseach topic on openmw forums for some background. diff --git a/apps/openmw/mwmechanics/pathgrid.cpp b/apps/openmw/mwmechanics/pathgrid.cpp index d380cba4e3..848d2c7a03 100644 --- a/apps/openmw/mwmechanics/pathgrid.cpp +++ b/apps/openmw/mwmechanics/pathgrid.cpp @@ -105,9 +105,7 @@ namespace MWMechanics mCell = cell->getCell(); mIsExterior = cell->getCell()->isExterior(); - mPathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*cell->getCell(), - MWBase::Environment::get().getWorld()->getCellName(cell)); - + mPathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*cell->getCell()); if(!mPathgrid) return false; diff --git a/apps/openmw/mwrender/debugging.cpp b/apps/openmw/mwrender/debugging.cpp index 553a6379f5..972c1b6dd0 100644 --- a/apps/openmw/mwrender/debugging.cpp +++ b/apps/openmw/mwrender/debugging.cpp @@ -233,7 +233,7 @@ void Debugging::enableCellPathgrid(MWWorld::CellStore *store) { MWBase::World* world = MWBase::Environment::get().getWorld(); const ESM::Pathgrid *pathgrid = - world->getStore().get().search(*store->getCell(), world->getCellName(store)); + world->getStore().get().search(*store->getCell()); if (!pathgrid) return; Vector3 cellPathGridPos(0, 0, 0); diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index 90786acd42..83e911174d 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -141,6 +141,8 @@ namespace MWWorld mStores[ESM::REC_SSCR] = &mStartScripts; mStores[ESM::REC_STAT] = &mStatics; mStores[ESM::REC_WEAP] = &mWeapons; + + mPathgrids.setCells(mCells); } void clearDynamic () diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index 1aaf902f85..c4070f0327 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -843,60 +843,103 @@ namespace MWWorld class Store : public StoreBase { private: - // 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. - // This is why we keep both interior and exterior pathgrids in the same container here. - typedef std::pair > PathgridKey; - typedef std::map Static; + typedef std::map Interior; + typedef std::map, ESM::Pathgrid> Exterior; - Static mStatic; + Interior mInt; + Exterior mExt; + + Store* mCells; public: + void setCells(Store& cells) + { + mCells = &cells; + } + void load(ESM::ESMReader &esm, const std::string &id) { ESM::Pathgrid pathgrid; pathgrid.load(esm); - PathgridKey key = std::make_pair(pathgrid.mCell, std::make_pair(pathgrid.mData.mX, pathgrid.mData.mY)); + // 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) != NULL; // Try to overwrite existing record - std::pair ret = mStatic.insert(std::make_pair(key, pathgrid)); - if (!ret.second) - ret.first->second = pathgrid; + 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; + } } size_t getSize() const { - return mStatic.size(); + return mInt.size() + mExt.size(); } void setUp() { } - const ESM::Pathgrid *search(const ESM::Cell &cell, const std::string& cellName) const { - int x=0,y=0; - if (!(cell.mData.mFlags & ESM::Cell::Interior)) - { - x = cell.mData.mX; - y = cell.mData.mY; - } - PathgridKey key = std::make_pair(cellName, std::make_pair(x,y)); - - Static::const_iterator it = mStatic.find(key); - if (it != mStatic.end()) + const ESM::Pathgrid *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 NULL; } - const ESM::Pathgrid *find(const ESM::Cell &cell, const std::string& cellName) const { - const ESM::Pathgrid* pathgrid = search(cell, cellName); - if (pathgrid == 0) { + const ESM::Pathgrid *search(const std::string& name) const { + Interior::const_iterator it = mInt.find(name); + if (it != mInt.end()) + return &(it->second); + return NULL; + } + + const ESM::Pathgrid *find(int x, int y) const { + const ESM::Pathgrid* pathgrid = search(x,y); + if (!pathgrid) + { std::ostringstream msg; - msg << "Pathgrid in cell '" << cellName << "' not found"; + msg << "Pathgrid in cell '" << x << " " << y << "' not found"; throw std::runtime_error(msg.str()); } return pathgrid; } + + const ESM::Pathgrid* find(const std::string& name) const { + const ESM::Pathgrid* pathgrid = search(name); + if (!pathgrid) + { + std::ostringstream msg; + msg << "Pathgrid in cell '" << name << "' not found"; + throw std::runtime_error(msg.str()); + } + return pathgrid; + } + + const ESM::Pathgrid *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 *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); + } }; template From 8ed376af5e09a975895fd858654c43f8318a46ad Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 11 Dec 2014 15:59:27 +0100 Subject: [PATCH 231/303] Launcher: fix changing active profile through the Play page --- apps/launcher/datafilespage.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 7192ed7842..3c4d36de77 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -206,6 +206,10 @@ void Launcher::DataFilesPage::slotProfileRenamed(const QString &previous, const 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); } From 7e8ca3fff1837b632145d9f076a5769d0c9a7fd8 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 11 Dec 2014 19:27:13 +0100 Subject: [PATCH 232/303] Fix object movement between cells producing a stale Ptr within the script execution (Bug #1942) --- apps/openmw/mwbase/world.hpp | 3 +- apps/openmw/mwscript/interpretercontext.cpp | 6 ++++ apps/openmw/mwscript/interpretercontext.hpp | 3 ++ .../mwscript/transformationextensions.cpp | 36 ++++++++++++------- apps/openmw/mwworld/worldimp.cpp | 10 +++--- apps/openmw/mwworld/worldimp.hpp | 7 ++-- 6 files changed, 45 insertions(+), 20 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 2dd135f3da..c674145aef 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -281,7 +281,8 @@ namespace MWBase virtual void deleteObject (const MWWorld::Ptr& ptr) = 0; virtual void undeleteObject (const MWWorld::Ptr& ptr) = 0; - virtual void moveObject (const MWWorld::Ptr& ptr, float x, float y, float z) = 0; + virtual MWWorld::Ptr moveObject (const MWWorld::Ptr& ptr, float x, float y, float z) = 0; + ///< @return an updated Ptr in case the Ptr's cell changes virtual void moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, float x, float y, float z) = 0; diff --git a/apps/openmw/mwscript/interpretercontext.cpp b/apps/openmw/mwscript/interpretercontext.cpp index d8d13a9211..430389e30c 100644 --- a/apps/openmw/mwscript/interpretercontext.cpp +++ b/apps/openmw/mwscript/interpretercontext.cpp @@ -590,4 +590,10 @@ namespace MWScript { return mTargetId; } + + void InterpreterContext::updatePtr(const MWWorld::Ptr& updated) + { + if (!mReference.isEmpty()) + mReference = updated; + } } diff --git a/apps/openmw/mwscript/interpretercontext.hpp b/apps/openmw/mwscript/interpretercontext.hpp index b543399656..7f3172dd1b 100644 --- a/apps/openmw/mwscript/interpretercontext.hpp +++ b/apps/openmw/mwscript/interpretercontext.hpp @@ -169,6 +169,9 @@ namespace MWScript MWWorld::Ptr getReference(bool required=true); ///< Reference, that the script is running from (can be empty) + void updatePtr(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. + virtual std::string getTargetId() const; }; } diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp index 8e6d925b7c..ac9dea4081 100644 --- a/apps/openmw/mwscript/transformationextensions.cpp +++ b/apps/openmw/mwscript/transformationextensions.cpp @@ -224,20 +224,23 @@ namespace MWScript float ay = ptr.getRefData().getPosition().pos[1]; float az = ptr.getRefData().getPosition().pos[2]; + MWWorld::Ptr updated = ptr; if(axis == "x") { - MWBase::Environment::get().getWorld()->moveObject(ptr,pos,ay,az); + updated = MWBase::Environment::get().getWorld()->moveObject(ptr,pos,ay,az); } else if(axis == "y") { - MWBase::Environment::get().getWorld()->moveObject(ptr,ax,pos,az); + updated = MWBase::Environment::get().getWorld()->moveObject(ptr,ax,pos,az); } else if(axis == "z") { - MWBase::Environment::get().getWorld()->moveObject(ptr,ax,ay,pos); + updated = MWBase::Environment::get().getWorld()->moveObject(ptr,ax,ay,pos); } else throw std::runtime_error ("invalid axis: " + axis); + + dynamic_cast(runtime.getContext()).updatePtr(updated); } }; @@ -317,6 +320,8 @@ namespace MWScript { MWBase::Environment::get().getWorld()->moveObject(ptr,store,x,y,z); ptr = MWWorld::Ptr(ptr.getBase(), store); + dynamic_cast(runtime.getContext()).updatePtr(ptr); + float ax = Ogre::Radian(ptr.getRefData().getPosition().rot[0]).valueDegrees(); float ay = Ogre::Radian(ptr.getRefData().getPosition().rot[1]).valueDegrees(); // Note that you must specify ZRot in minutes (1 degree = 60 minutes; north = 0, east = 5400, south = 10800, west = 16200) @@ -365,15 +370,18 @@ namespace MWScript // 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 updated; if (ptr.getRefData().getHandle() == "player") { - MWBase::Environment::get().getWorld()->moveObject(ptr, - MWBase::Environment::get().getWorld()->getExterior(cx,cy),x,y,z); + MWWorld::CellStore* cell = MWBase::Environment::get().getWorld()->getExterior(cx,cy); + MWBase::Environment::get().getWorld()->moveObject(ptr,cell,x,y,z); + updated = MWWorld::Ptr(ptr.getBase(), cell); } else { - MWBase::Environment::get().getWorld()->moveObject(ptr, x, y, z); + updated = MWBase::Environment::get().getWorld()->moveObject(ptr, x, y, z); } + dynamic_cast(runtime.getContext()).updatePtr(updated); float ax = Ogre::Radian(ptr.getRefData().getPosition().rot[0]).valueDegrees(); float ay = Ogre::Radian(ptr.getRefData().getPosition().rot[1]).valueDegrees(); @@ -638,8 +646,10 @@ namespace MWScript ptr.getRefData().setLocalRotation(rot); MWBase::Environment::get().getWorld()->rotateObject(ptr, 0,0,0,true); - MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], - ptr.getCellRef().getPosition().pos[1], ptr.getCellRef().getPosition().pos[2]); + + dynamic_cast(runtime.getContext()).updatePtr( + MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], + ptr.getCellRef().getPosition().pos[1], ptr.getCellRef().getPosition().pos[2])); } }; @@ -678,7 +688,8 @@ namespace MWScript throw std::runtime_error ("invalid movement axis: " + axis); Ogre::Vector3 worldPos = ptr.getRefData().getBaseNode()->convertLocalToWorldPosition(posChange); - MWBase::Environment::get().getWorld()->moveObject(ptr, worldPos.x, worldPos.y, worldPos.z); + dynamic_cast(runtime.getContext()).updatePtr( + MWBase::Environment::get().getWorld()->moveObject(ptr, worldPos.x, worldPos.y, worldPos.z)); } }; @@ -701,17 +712,18 @@ namespace MWScript const float *objPos = ptr.getRefData().getPosition().pos; + MWWorld::Ptr updated; if (axis == "x") { - MWBase::Environment::get().getWorld()->moveObject(ptr, objPos[0]+movement, objPos[1], objPos[2]); + updated = MWBase::Environment::get().getWorld()->moveObject(ptr, objPos[0]+movement, objPos[1], objPos[2]); } else if (axis == "y") { - MWBase::Environment::get().getWorld()->moveObject(ptr, objPos[0], objPos[1]+movement, objPos[2]); + updated = MWBase::Environment::get().getWorld()->moveObject(ptr, objPos[0], objPos[1]+movement, objPos[2]); } else if (axis == "z") { - MWBase::Environment::get().getWorld()->moveObject(ptr, objPos[0], objPos[1], objPos[2]+movement); + updated = MWBase::Environment::get().getWorld()->moveObject(ptr, objPos[0], objPos[1], objPos[2]+movement); } else throw std::runtime_error ("invalid movement axis: " + axis); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 2607a6d4d0..75de050e15 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1187,7 +1187,7 @@ namespace MWWorld } } - bool World::moveObjectImp(const Ptr& ptr, float x, float y, float z) + MWWorld::Ptr World::moveObjectImp(const Ptr& ptr, float x, float y, float z) { CellStore *cell = ptr.getCell(); @@ -1200,12 +1200,14 @@ namespace MWWorld moveObject(ptr, cell, x, y, z); - return cell != ptr.getCell(); + MWWorld::Ptr updated = ptr; + updated.mCell = cell; + return updated; } - void World::moveObject (const Ptr& ptr, float x, float y, float z) + MWWorld::Ptr World::moveObject (const Ptr& ptr, float x, float y, float z) { - moveObjectImp(ptr, x, y, z); + return moveObjectImp(ptr, x, y, z); } void World::scaleObject (const Ptr& ptr, float scale) diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 555ed7fb74..1548d0c87a 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -104,8 +104,8 @@ namespace MWWorld void rotateObjectImp (const Ptr& ptr, Ogre::Vector3 rot, bool adjust); - bool moveObjectImp (const Ptr& ptr, float x, float y, float z); - ///< @return true if the active cell (cell player is in) changed + Ptr moveObjectImp (const Ptr& ptr, float x, float y, float z); + ///< @return an updated Ptr in case the Ptr's cell changes Ptr copyObjectToCell(const Ptr &ptr, CellStore* cell, ESM::Position pos, bool adjustPos=true); @@ -341,7 +341,8 @@ namespace MWWorld virtual void deleteObject (const Ptr& ptr); virtual void undeleteObject (const Ptr& ptr); - virtual void moveObject (const Ptr& ptr, float x, float y, float z); + virtual MWWorld::Ptr moveObject (const Ptr& ptr, float x, float y, float z); + ///< @return an updated Ptr in case the Ptr's cell changes virtual void moveObject (const Ptr& ptr, CellStore* newCell, float x, float y, float z); virtual void scaleObject (const Ptr& ptr, float scale); From ed2aa5a233e16d3d2fa8419607e866f67a7a5589 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 11 Dec 2014 20:32:05 +0100 Subject: [PATCH 233/303] Fix crash caused by dangling baseNode pointer --- apps/openmw/mwworld/worldimp.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 75de050e15..e24e99c305 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1158,6 +1158,7 @@ namespace MWWorld ptr.getClass().copyToCell(ptr, *newCell, pos); mRendering->updateObjectCell(ptr, copy); + ptr.getRefData().setBaseNode(NULL); MWBase::Environment::get().getSoundManager()->updatePtr (ptr, copy); MWBase::MechanicsManager *mechMgr = MWBase::Environment::get().getMechanicsManager(); From d955017079e69b46b60ba8824c181f3afb543daa Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 11 Dec 2014 20:51:02 +0100 Subject: [PATCH 234/303] Don't report script operation status via messageBox (Bug #1942) --- apps/openmw/mwscript/interpretercontext.cpp | 1 - apps/openmw/mwscript/interpretercontext.hpp | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/openmw/mwscript/interpretercontext.cpp b/apps/openmw/mwscript/interpretercontext.cpp index d8d13a9211..88aa46a98f 100644 --- a/apps/openmw/mwscript/interpretercontext.cpp +++ b/apps/openmw/mwscript/interpretercontext.cpp @@ -205,7 +205,6 @@ namespace MWScript void InterpreterContext::report (const std::string& message) { - messageBox (message); } bool InterpreterContext::menuMode() diff --git a/apps/openmw/mwscript/interpretercontext.hpp b/apps/openmw/mwscript/interpretercontext.hpp index b543399656..bcf02e68e9 100644 --- a/apps/openmw/mwscript/interpretercontext.hpp +++ b/apps/openmw/mwscript/interpretercontext.hpp @@ -78,7 +78,7 @@ namespace MWScript const std::vector& buttons); virtual void report (const std::string& message); - ///< By default echo via messageBox. + ///< By default, do nothing. virtual bool menuMode(); From be16f1d0a53738f3a67c9278031476a2a9820838 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 11 Dec 2014 20:57:25 +0100 Subject: [PATCH 235/303] Implement PcForce1stPerson, PcForce3rdPerson, PcGet3rdPerson (Bug #2078) --- apps/openmw/mwbase/world.hpp | 1 + apps/openmw/mwscript/docs/vmformat.txt | 5 +++- apps/openmw/mwscript/miscextensions.cpp | 32 +++++++++++++++++++++++++ apps/openmw/mwworld/worldimp.hpp | 4 ++++ components/compiler/extensions0.cpp | 3 +++ components/compiler/opcodes.hpp | 3 +++ 6 files changed, 47 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 2dd135f3da..2bec88998d 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -387,6 +387,7 @@ namespace MWBase virtual bool isOnGround(const MWWorld::Ptr &ptr) const = 0; virtual void togglePOV() = 0; + virtual bool isFirstPerson() const = 0; virtual void togglePreviewMode(bool enable) = 0; virtual bool toggleVanityMode(bool enable) = 0; virtual void allowVanityMode(bool allow) = 0; diff --git a/apps/openmw/mwscript/docs/vmformat.txt b/apps/openmw/mwscript/docs/vmformat.txt index b80c84d674..c90f63f7fa 100644 --- a/apps/openmw/mwscript/docs/vmformat.txt +++ b/apps/openmw/mwscript/docs/vmformat.txt @@ -433,5 +433,8 @@ op 0x20002c4-0x20002db: ModMagicEffect op 0x20002dc-0x20002f3: ModMagicEffect, explicit op 0x20002f4: ResetActors op 0x20002f5: ToggleWorld +op 0x20002f6: PCForce1stPerson +op 0x20002f7: PCForce3rdPerson +op 0x20002f8: PCGet3rdPerson -opcodes 0x20002f6-0x3ffffff unused +opcodes 0x20002f9-0x3ffffff unused diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index ec2048e11f..186aa708db 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -312,6 +312,35 @@ namespace MWScript } }; + class OpPcForce1stPerson : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + if (!MWBase::Environment::get().getWorld()->isFirstPerson()) + MWBase::Environment::get().getWorld()->togglePOV(); + } + }; + + class OpPcForce3rdPerson : public Interpreter::Opcode0 + { + virtual void execute (Interpreter::Runtime& runtime) + { + if (MWBase::Environment::get().getWorld()->isFirstPerson()) + MWBase::Environment::get().getWorld()->togglePOV(); + } + }; + + class OpPcGet3rdPerson : public Interpreter::Opcode0 + { + public: + virtual void execute(Interpreter::Runtime& runtime) + { + runtime.push(!MWBase::Environment::get().getWorld()->isFirstPerson()); + } + }; + class OpToggleVanityMode : public Interpreter::Opcode0 { static bool sActivate; @@ -1002,6 +1031,9 @@ namespace MWScript 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); diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 555ed7fb74..c93d517c19 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -451,6 +451,10 @@ namespace MWWorld mRendering->togglePOV(); } + virtual bool isFirstPerson() const { + return mRendering->getCamera()->isFirstPerson(); + } + virtual void togglePreviewMode(bool enable) { mRendering->togglePreviewMode(enable); } diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index 7531cdd5bf..cd5bdbe695 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -261,6 +261,9 @@ namespace Compiler extensions.registerInstruction ("togglepathgrid", "", opcodeTogglePathgrid); extensions.registerInstruction ("tpg", "", opcodeTogglePathgrid); extensions.registerInstruction ("dontsaveobject", "", opcodeDontSaveObject); + extensions.registerInstruction ("pcforce1stperson", "", opcodePcForce1stPerson); + extensions.registerInstruction ("pcforce3rdperson", "", opcodePcForce3rdPerson); + extensions.registerFunction ("pcget3rdperson", 'l', "", opcodePcGet3rdPerson); extensions.registerInstruction ("togglevanitymode", "", opcodeToggleVanityMode); extensions.registerInstruction ("tvm", "", opcodeToggleVanityMode); extensions.registerFunction ("getpcsleep", 'l', "", opcodeGetPcSleep); diff --git a/components/compiler/opcodes.hpp b/components/compiler/opcodes.hpp index 5063397e11..65efc14fa4 100644 --- a/components/compiler/opcodes.hpp +++ b/components/compiler/opcodes.hpp @@ -215,6 +215,9 @@ namespace Compiler const int opcodeToggleWorld = 0x20002f5; const int opcodeTogglePathgrid = 0x2000146; const int opcodeDontSaveObject = 0x2000153; + const int opcodePcForce1stPerson = 0x20002f6; + const int opcodePcForce3rdPerson = 0x20002f7; + const int opcodePcGet3rdPerson = 0x20002f8; const int opcodeToggleVanityMode = 0x2000174; const int opcodeGetPcSleep = 0x200019f; const int opcodeGetPcJumping = 0x2000233; From a355550cabcf9383f35626d1c9f5f4b10c55a4b1 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 11 Dec 2014 21:43:31 +0100 Subject: [PATCH 236/303] Add support for NPCs with missing head/hair models (Fixes #2078) --- apps/openmw/mwrender/npcanimation.cpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index f2bc3df95f..4b5ddcbec3 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -56,8 +56,13 @@ std::string getVampireHead(const std::string& race, bool female) } } - assert(sVampireMapping[thisCombination]); - return "meshes\\" + sVampireMapping[thisCombination]->mModel; + if (sVampireMapping.find(thisCombination) == sVampireMapping.end()) + sVampireMapping[thisCombination] = NULL; + + const ESM::BodyPart* bodyPart = sVampireMapping[thisCombination]; + if (!bodyPart) + return std::string(); + return "meshes\\" + bodyPart->mModel; } bool isSkinned (NifOgre::ObjectScenePtr scene) @@ -256,10 +261,15 @@ void NpcAnimation::updateNpcBase() { if (isVampire) mHeadModel = getVampireHead(mNpc->mRace, mNpc->mFlags & ESM::NPC::Female); - else + else if (!mNpc->mHead.empty()) mHeadModel = "meshes\\" + store.get().find(mNpc->mHead)->mModel; + else + mHeadModel = ""; - mHairModel = "meshes\\" + store.get().find(mNpc->mHair)->mModel; + if (!mNpc->mHair.empty()) + mHairModel = "meshes\\" + store.get().find(mNpc->mHair)->mModel; + else + mHairModel = ""; } bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; @@ -399,9 +409,9 @@ void NpcAnimation::updateParts() if(mViewMode != VM_FirstPerson) { - if(mPartPriorities[ESM::PRT_Head] < 1) + 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) + if(mPartPriorities[ESM::PRT_Hair] < 1 && mPartPriorities[ESM::PRT_Head] <= 1 && !mHairModel.empty()) addOrReplaceIndividualPart(ESM::PRT_Hair, -1,1, mHairModel); } if(mViewMode == VM_HeadOnly) From 5f00a3d5c3f0b934f21c4742ec8a7b6d4dec8cd3 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 11 Dec 2014 22:00:31 +0100 Subject: [PATCH 237/303] Reset lastHitObject when it is retrieved rather than every frame This seems to be how vanilla MW does it. --- apps/openmw/mwmechanics/actors.cpp | 9 --------- apps/openmw/mwscript/miscextensions.cpp | 2 ++ 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index ae430e5c6a..899ab95088 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1122,15 +1122,6 @@ namespace MWMechanics // target lists get updated once every 1.0 sec if (timerUpdateAITargets >= 1.0f) timerUpdateAITargets = 0; - // Reset data from previous frame - for (PtrControllerMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) - { - // Reset last hit object, which is only valid for one frame - // Note, the new hit object for this frame may be set by CharacterController::update -> Animation::runAnimation - // (below) - iter->first.getClass().getCreatureStats(iter->first).setLastHitObject(std::string()); - } - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); int hostilesCount = 0; // need to know this to play Battle music diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 186aa708db..1d07c95915 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -732,6 +732,8 @@ namespace MWScript MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); runtime.push(::Misc::StringUtils::ciEqual(objectID, stats.getLastHitObject())); + + stats.setLastHitObject(std::string()); } }; From 886903d70e62334cfeb5de1939ee457017462ca9 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 11 Dec 2014 22:25:41 +0100 Subject: [PATCH 238/303] Implement HitAttemptOnMe function (Bug #2078) --- apps/openmw/mwclass/creature.cpp | 5 +++-- apps/openmw/mwclass/npc.cpp | 5 +++-- apps/openmw/mwmechanics/creaturestats.cpp | 12 ++++++++++++ apps/openmw/mwmechanics/creaturestats.hpp | 3 +++ apps/openmw/mwscript/docs/vmformat.txt | 4 +++- apps/openmw/mwscript/miscextensions.cpp | 21 +++++++++++++++++++++ components/compiler/extensions0.cpp | 1 + components/compiler/opcodes.hpp | 2 ++ components/esm/creaturestats.cpp | 5 +++++ components/esm/creaturestats.hpp | 1 + 10 files changed, 54 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 5910c471b3..8076e0619c 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -346,10 +346,11 @@ namespace MWClass setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); } + if(!object.isEmpty()) + getCreatureStats(ptr).setLastHitAttemptObject(object.getClass().getId(object)); + if(!successful) { - // TODO: Handle HitAttemptOnMe script function - // Missed MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "miss", 1.0f, 1.0f); return; diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 49f76d25c6..6806558833 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -654,10 +654,11 @@ namespace MWClass setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); } + if(!object.isEmpty()) + getCreatureStats(ptr).setLastHitAttemptObject(object.getClass().getId(object)); + if(!successful) { - // TODO: Handle HitAttemptOnMe script function - // Missed sndMgr->playSound3D(ptr, "miss", 1.0f, 1.0f); return; diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 21fd2203fd..72a710c656 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -359,6 +359,16 @@ namespace MWMechanics return mLastHitObject; } + void CreatureStats::setLastHitAttemptObject(const std::string& objectid) + { + mLastHitAttemptObject = objectid; + } + + const std::string &CreatureStats::getLastHitAttemptObject() const + { + return mLastHitAttemptObject; + } + void CreatureStats::addToFallHeight(float height) { mFallHeight += height; @@ -510,6 +520,7 @@ namespace MWMechanics state.mAttackStrength = mAttackStrength; state.mFallHeight = mFallHeight; // TODO: vertical velocity (move from PhysicActor to CreatureStats?) state.mLastHitObject = mLastHitObject; + state.mLastHitAttemptObject = mLastHitAttemptObject; state.mRecalcDynamicStats = mRecalcMagicka; state.mDrawState = mDrawState; state.mLevel = mLevel; @@ -558,6 +569,7 @@ namespace MWMechanics mAttackStrength = state.mAttackStrength; mFallHeight = state.mFallHeight; mLastHitObject = state.mLastHitObject; + mLastHitAttemptObject = state.mLastHitAttemptObject; mRecalcMagicka = state.mRecalcDynamicStats; mDrawState = DrawState_(state.mDrawState); mLevel = state.mLevel; diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index f830dd310c..d13ced3b3a 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -52,6 +52,7 @@ namespace MWMechanics 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; @@ -241,7 +242,9 @@ namespace MWMechanics bool getStance (Stance flag) const; void setLastHitObject(const std::string &objectid); + void setLastHitAttemptObject(const std::string &objectid); const std::string &getLastHitObject() const; + const std::string &getLastHitAttemptObject() 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? diff --git a/apps/openmw/mwscript/docs/vmformat.txt b/apps/openmw/mwscript/docs/vmformat.txt index c90f63f7fa..800c6e2c7b 100644 --- a/apps/openmw/mwscript/docs/vmformat.txt +++ b/apps/openmw/mwscript/docs/vmformat.txt @@ -436,5 +436,7 @@ op 0x20002f5: ToggleWorld op 0x20002f6: PCForce1stPerson op 0x20002f7: PCForce3rdPerson op 0x20002f8: PCGet3rdPerson +op 0x20002f9: HitAttemptOnMe +op 0x20002fa: HitAttemptOnMe, explicit -opcodes 0x20002f9-0x3ffffff unused +opcodes 0x20002fb-0x3ffffff unused diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 1d07c95915..c92acff820 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -737,6 +737,25 @@ namespace MWScript } }; + template + class OpHitAttemptOnMe : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + 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 { @@ -1085,6 +1104,8 @@ namespace MWScript 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); diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index cd5bdbe695..1e2aebd991 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -295,6 +295,7 @@ namespace Compiler extensions.registerInstruction ("hurtcollidingactor", "f", opcodeHurtCollidingActor, opcodeHurtCollidingActorExplicit); extensions.registerFunction ("getwindspeed", 'f', "", opcodeGetWindSpeed); extensions.registerFunction ("hitonme", 'l', "S", opcodeHitOnMe, opcodeHitOnMeExplicit); + extensions.registerFunction ("hitattemptonme", 'l', "S", opcodeHitAttemptOnMe, opcodeHitAttemptOnMeExplicit); extensions.registerInstruction ("disableteleporting", "", opcodeDisableTeleporting); extensions.registerInstruction ("enableteleporting", "", opcodeEnableTeleporting); extensions.registerInstruction ("showvars", "", opcodeShowVars, opcodeShowVarsExplicit); diff --git a/components/compiler/opcodes.hpp b/components/compiler/opcodes.hpp index 65efc14fa4..da79555e22 100644 --- a/components/compiler/opcodes.hpp +++ b/components/compiler/opcodes.hpp @@ -269,6 +269,8 @@ namespace Compiler const int opcodePayFineThief = 0x2000237; const int opcodeHitOnMe = 0x2000213; const int opcodeHitOnMeExplicit = 0x2000214; + const int opcodeHitAttemptOnMe = 0x20002f9; + const int opcodeHitAttemptOnMeExplicit = 0x20002fa; const int opcodeDisableTeleporting = 0x2000215; const int opcodeEnableTeleporting = 0x2000216; const int opcodeShowVars = 0x200021d; diff --git a/components/esm/creaturestats.cpp b/components/esm/creaturestats.cpp index 21803e02f9..cc76ef0df7 100644 --- a/components/esm/creaturestats.cpp +++ b/components/esm/creaturestats.cpp @@ -68,6 +68,8 @@ void ESM::CreatureStats::load (ESMReader &esm) mLastHitObject = esm.getHNOString ("LHIT"); + mLastHitAttemptObject = esm.getHNOString ("LHAT"); + mRecalcDynamicStats = false; esm.getHNOT (mRecalcDynamicStats, "CALC"); @@ -179,6 +181,9 @@ void ESM::CreatureStats::save (ESMWriter &esm) const if (!mLastHitObject.empty()) esm.writeHNString ("LHIT", mLastHitObject); + if (!mLastHitAttemptObject.empty()) + esm.writeHNString ("LHAT", mLastHitAttemptObject); + if (mRecalcDynamicStats) esm.writeHNT ("CALC", mRecalcDynamicStats); diff --git a/components/esm/creaturestats.hpp b/components/esm/creaturestats.hpp index 8f4d4df7b8..7946d0e45b 100644 --- a/components/esm/creaturestats.hpp +++ b/components/esm/creaturestats.hpp @@ -56,6 +56,7 @@ namespace ESM float mAttackStrength; float mFallHeight; std::string mLastHitObject; + std::string mLastHitAttemptObject; bool mRecalcDynamicStats; int mDrawState; unsigned char mDeathAnimation; From 7892ed35f32c4c322043f706be801c537201c34e Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 11 Dec 2014 22:25:53 +0100 Subject: [PATCH 239/303] PlaceItem, PlaceItemCell: Make sure references are placed above terrain (Bug #2078) --- apps/openmw/mwscript/transformationextensions.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp index 8e6d925b7c..80b13d6bb3 100644 --- a/apps/openmw/mwscript/transformationextensions.cpp +++ b/apps/openmw/mwscript/transformationextensions.cpp @@ -433,7 +433,8 @@ namespace MWScript pos.rot[2] = zRot; MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(),itemID); ref.getPtr().getCellRef().setPosition(pos); - MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(),store,pos); + MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(),store,pos); + placed.getClass().adjustPosition(placed, true); } else { @@ -480,7 +481,8 @@ namespace MWScript pos.rot[2] = zRot; MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(),itemID); ref.getPtr().getCellRef().setPosition(pos); - MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(),store,pos); + MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(),store,pos); + placed.getClass().adjustPosition(placed, true); } }; From f42420bc199322ebcc6f7ff3ba3ce06fac96ebbe Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 12 Dec 2014 01:24:35 +0100 Subject: [PATCH 240/303] Use the Original Creature field for SoundGen lookups --- apps/openmw/mwclass/creature.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 8076e0619c..d3c216c2b2 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -675,13 +675,12 @@ namespace MWClass std::vector sounds; sounds.reserve(8); - std::string ptrid = Creature::getId(ptr); + MWWorld::LiveCellRef* ref = ptr.get(); + MWWorld::Store::iterator sound = store.begin(); while(sound != store.end()) { - if(type == sound->mType && !sound->mCreature.empty() && - Misc::StringUtils::ciEqual(ptrid.substr(0, sound->mCreature.size()), - sound->mCreature)) + if (type == sound->mType && !sound->mCreature.empty() && Misc::StringUtils::ciEqual(ref->mBase->mOriginal, sound->mCreature)) sounds.push_back(&*sound); ++sound; } From cf5fc60e861d946fe2283bd7ac3369952381bf5c Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 12 Dec 2014 01:42:13 +0100 Subject: [PATCH 241/303] Make ToggleMenus affect tooltips (Fixes #1989) --- apps/openmw/mwgui/windowmanagerimp.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 48f28d300c..99a43fbffd 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -444,6 +444,7 @@ namespace MWGui mVideoBackground->setVisible(false); mHud->setVisible(mHudEnabled && mGuiEnabled); + mToolTips->setVisible(mGuiEnabled); bool gameMode = !isGuiMode(); From e69cf110292c5a71525cebc446e97a8c2f5d58c8 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 12 Dec 2014 02:05:26 +0100 Subject: [PATCH 242/303] Hide tooltips during loading screens --- apps/openmw/mwgui/windowmanagerimp.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 99a43fbffd..cbcef3bbd8 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -589,6 +589,7 @@ namespace MWGui break; case GM_LoadingWallpaper: mHud->setVisible(false); + mToolTips->setVisible(false); setCursorVisible(false); break; case GM_Loading: @@ -597,7 +598,7 @@ namespace MWGui mStatsWindow->setVisible(mStatsWindow->pinned() && !(mForceHidden & GW_Stats)); mInventoryWindow->setVisible(mInventoryWindow->pinned() && !(mForceHidden & GW_Inventory)); mSpellWindow->setVisible(mSpellWindow->pinned() && !(mForceHidden & GW_Magic)); - + mToolTips->setVisible(false); setCursorVisible(false); break; default: From 03da21f088da16c322f5046354141082a6d5dfc0 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 12 Dec 2014 02:12:49 +0100 Subject: [PATCH 243/303] Remove redundant GUI element showing during loading screens --- apps/openmw/mwgui/windowmanagerimp.cpp | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index cbcef3bbd8..acb8b2eb75 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -588,16 +588,10 @@ namespace MWGui mJournal->setVisible(true); break; case GM_LoadingWallpaper: - mHud->setVisible(false); - mToolTips->setVisible(false); - setCursorVisible(false); - break; case GM_Loading: - // Show the pinned windows - mMap->setVisible(mMap->pinned() && !(mForceHidden & GW_Map)); - mStatsWindow->setVisible(mStatsWindow->pinned() && !(mForceHidden & GW_Stats)); - mInventoryWindow->setVisible(mInventoryWindow->pinned() && !(mForceHidden & GW_Inventory)); - mSpellWindow->setVisible(mSpellWindow->pinned() && !(mForceHidden & GW_Magic)); + // Don't need to show anything here - GM_LoadingWallpaper covers everything else anyway, + // GM_Loading uses a texture of the last rendered frame so everything previously visible will be rendered. + mHud->setVisible(false); mToolTips->setVisible(false); setCursorVisible(false); break; From bc85bb32c21cf4a1e932991eebe7e0f3d2dbd0eb Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 12 Dec 2014 02:39:59 +0100 Subject: [PATCH 244/303] Fix vampirism magic effect not applying immediately (Fixes #1984) --- apps/openmw/mwmechanics/character.cpp | 8 +++++--- apps/openmw/mwmechanics/character.hpp | 2 +- apps/openmw/mwrender/animation.hpp | 1 + apps/openmw/mwrender/npcanimation.cpp | 10 ++++++++++ apps/openmw/mwrender/npcanimation.hpp | 2 ++ 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 87d9a5b679..4d74133aa3 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1279,7 +1279,7 @@ void CharacterController::update(float duration) const MWWorld::Class &cls = mPtr.getClass(); Ogre::Vector3 movement(0.0f); - updateVisibility(); + updateMagicEffects(); if(!cls.isActor()) { @@ -1777,7 +1777,7 @@ void CharacterController::updateContinuousVfx() } } -void CharacterController::updateVisibility() +void CharacterController::updateMagicEffects() { if (!mPtr.getClass().isActor()) return; @@ -1794,9 +1794,11 @@ void CharacterController::updateVisibility() { alpha *= std::max(0.2f, (100.f - chameleon)/100.f); } - mAnimation->setAlpha(alpha); + bool vampire = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude() > 0.0f; + mAnimation->setVampire(vampire); + float light = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Light).getMagnitude(); mAnimation->setLightEffect(light); } diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 2a14c53b91..3409efedfd 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -190,7 +190,7 @@ class CharacterController void castSpell(const std::string& spellid); - void updateVisibility(); + void updateMagicEffects(); void playDeath(float startpoint, CharacterState death); void playRandomDeath(float startpoint = 0.0f); diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index a8a9ee11e6..d25d4f0b0e 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -228,6 +228,7 @@ public: virtual void preRender (Ogre::Camera* camera); virtual void setAlpha(float alpha) {} + virtual void setVampire(bool vampire) {} public: void updatePtr(const MWWorld::Ptr &ptr); diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 4b5ddcbec3..b93b37aeac 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -983,4 +983,14 @@ void NpcAnimation::equipmentChanged() updateParts(); } +void NpcAnimation::setVampire(bool vampire) +{ + if (mNpcType == Type_Werewolf) // we can't have werewolf vampires, can we + return; + if ((mNpcType == Type_Vampire) != vampire) + { + rebuild(); + } +} + } diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index ee62fce9cd..6631c8f412 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -168,6 +168,8 @@ public: /// Make the NPC only partially visible virtual void setAlpha(float alpha); + virtual void setVampire(bool vampire); + /// Prepare this animation for being rendered with \a camera (rotates billboard nodes) virtual void preRender (Ogre::Camera* camera); }; From 018f4e6895177c8290d52ca3b49c0319a4ecdfd2 Mon Sep 17 00:00:00 2001 From: Arthur Moore Date: Sun, 30 Nov 2014 18:59:05 -0500 Subject: [PATCH 245/303] Fail early if trying to read a string larger than the nif file size. This is much better than failing after a few minutes with an out of memory error. --- components/nif/nifstream.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/nif/nifstream.cpp b/components/nif/nifstream.cpp index a6fd5ef5ae..1b5f715bc7 100644 --- a/components/nif/nifstream.cpp +++ b/components/nif/nifstream.cpp @@ -83,6 +83,11 @@ Transformation NIFStream::getTrafo() std::string NIFStream::getString(size_t length) { + //Make sure we're not reading in too large of a string + unsigned int fileSize = inp->size(); + if(fileSize != 0 && fileSize < length) + file->fail("Attempted to read a string with " + Ogre::StringConverter::toString(length) + "characters , but file is only "+Ogre::StringConverter::toString(fileSize)+ "bytes!"); + std::vector str (length+1, 0); if(inp->read(&str[0], length) != length) From cd835152e109dfc5bcbd41812c6ce80f11ba6c4e Mon Sep 17 00:00:00 2001 From: Arthur Moore Date: Fri, 12 Dec 2014 01:36:10 -0500 Subject: [PATCH 246/303] Fix spacing issue for NIF file errors. --- components/nif/niffile.hpp | 4 ++-- components/nif/nifstream.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/nif/niffile.hpp b/components/nif/niffile.hpp index 2ef2a6fda2..ceb9984fb8 100644 --- a/components/nif/niffile.hpp +++ b/components/nif/niffile.hpp @@ -46,14 +46,14 @@ public: /// Used if file parsing fails void fail(const std::string &msg) { - std::string err = "NIFFile Error: " + msg; + std::string err = " NIFFile Error: " + msg; err += "\nFile: " + filename; throw std::runtime_error(err); } /// Used when something goes wrong, but not catastrophically so void warn(const std::string &msg) { - std::cerr << "NIFFile Warning: " << msg <size(); if(fileSize != 0 && fileSize < length) - file->fail("Attempted to read a string with " + Ogre::StringConverter::toString(length) + "characters , but file is only "+Ogre::StringConverter::toString(fileSize)+ "bytes!"); + file->fail("Attempted to read a string with " + Ogre::StringConverter::toString(length) + " characters , but file is only "+Ogre::StringConverter::toString(fileSize)+ " bytes!"); std::vector str (length+1, 0); From b8edd9bac33ad0f0c8f0701ebc8b020e5368ab51 Mon Sep 17 00:00:00 2001 From: Arthur Moore Date: Fri, 12 Dec 2014 01:48:57 -0500 Subject: [PATCH 247/303] Get a nif file's version string regardless of its length. --- components/nif/niffile.cpp | 4 ++-- components/nif/nifstream.cpp | 4 ++++ components/nif/nifstream.hpp | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/components/nif/niffile.cpp b/components/nif/niffile.cpp index c689e27b3a..9a544f3c4e 100644 --- a/components/nif/niffile.cpp +++ b/components/nif/niffile.cpp @@ -131,9 +131,9 @@ void NIFFile::parse() NIFStream nif (this, Ogre::ResourceGroupManager::getSingleton().openResource(filename)); // Check the header string - std::string head = nif.getString(40); + std::string head = nif.getVersionString(); if(head.compare(0, 22, "NetImmerse File Format") != 0) - fail("Invalid NIF header"); + fail("Invalid NIF header: " + head); // Get BCD version ver = nif.getUInt(); diff --git a/components/nif/nifstream.cpp b/components/nif/nifstream.cpp index 878b6b75f7..e5699db7b9 100644 --- a/components/nif/nifstream.cpp +++ b/components/nif/nifstream.cpp @@ -101,6 +101,10 @@ std::string NIFStream::getString() size_t size = read_le32(); return getString(size); } +std::string NIFStream::getVersionString() +{ + return inp->getLine(); +} void NIFStream::getShorts(std::vector &vec, size_t size) { diff --git a/components/nif/nifstream.hpp b/components/nif/nifstream.hpp index 3d6a1319a8..cc14971fd5 100644 --- a/components/nif/nifstream.hpp +++ b/components/nif/nifstream.hpp @@ -81,6 +81,8 @@ public: std::string getString(size_t length); ///Read in a string of the length specified in the file std::string getString(); + ///This is special since the version string doesn't start with a number, and ends with "\n" + std::string getVersionString(); void getShorts(std::vector &vec, size_t size); void getFloats(std::vector &vec, size_t size); From 74e341b2bcc3ec516d5ab14e715fc928a057c995 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Fri, 12 Dec 2014 10:09:09 +0100 Subject: [PATCH 248/303] updated changelog again --- readme.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.txt b/readme.txt index 83f139a1d8..f5e3e0e59b 100644 --- a/readme.txt +++ b/readme.txt @@ -101,11 +101,13 @@ CHANGELOG 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 From d034a079e60b8014802f92522b0dab46be47f3a9 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 12 Dec 2014 16:49:22 +0100 Subject: [PATCH 249/303] Allow equipping twohanded weapon and shield at the same time (Fixes #1785) The shield can be equipped, meaning armor rating and item enchantments apply, but can not be blocked with. --- apps/openmw/mwbase/mechanicsmanager.hpp | 2 ++ apps/openmw/mwmechanics/actors.cpp | 9 ++++++ apps/openmw/mwmechanics/actors.hpp | 2 ++ apps/openmw/mwmechanics/character.cpp | 32 ++++++++++++++++--- apps/openmw/mwmechanics/character.hpp | 4 +++ apps/openmw/mwmechanics/combat.cpp | 9 +----- .../mwmechanics/mechanicsmanagerimp.cpp | 5 +++ .../mwmechanics/mechanicsmanagerimp.hpp | 2 ++ apps/openmw/mwrender/characterpreview.cpp | 7 +++- apps/openmw/mwrender/npcanimation.hpp | 2 +- apps/openmw/mwworld/actionequip.cpp | 6 +--- apps/openmw/mwworld/inventorystore.cpp | 6 +--- 12 files changed, 61 insertions(+), 25 deletions(-) diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index b7af1cbf72..c92459183d 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -205,6 +205,8 @@ namespace MWBase /// Resurrects the player if necessary virtual void keepPlayerAlive() = 0; + + virtual bool isReadyToBlock (const MWWorld::Ptr& ptr) const = 0; }; } diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 899ab95088..a64b6c57d4 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1554,4 +1554,13 @@ namespace MWMechanics if (ptr.getClass().isNpc()) calculateNpcStatModifiers(ptr, 0.f); } + + bool Actors::isReadyToBlock(const MWWorld::Ptr &ptr) const + { + PtrControllerMap::const_iterator it = mActors.find(ptr); + if (it == mActors.end()) + return false; + + return it->second->isReadyToBlock(); + } } diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index a24095c702..a30a9dcf01 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -125,6 +125,8 @@ namespace MWMechanics void clear(); // Clear death counter + bool isReadyToBlock(const MWWorld::Ptr& ptr) const; + private: PtrControllerMap mActors; diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 4d74133aa3..06450ddb3c 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -648,7 +648,8 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim mAnimation->showWeapons(true); mAnimation->setWeaponGroup(mCurrentWeapon); } - mAnimation->showCarriedLeft(mWeaponType != WeapType_Spell && mWeaponType != WeapType_HandToHand); + + mAnimation->showCarriedLeft(updateCarriedLeftVisible(mWeaponType)); } if(!cls.getCreatureStats(mPtr).isDead()) @@ -836,6 +837,25 @@ bool CharacterController::updateCreatureState() return false; } +bool CharacterController::updateCarriedLeftVisible(WeaponType 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. + switch (weaptype) + { + case WeapType_Spell: + case WeapType_BowAndArrow: + case WeapType_Crossbow: + case WeapType_HandToHand: + case WeapType_TwoHand: + case WeapType_TwoWide: + return false; + default: + return true; + } +} + bool CharacterController::updateWeaponState() { const MWWorld::Class &cls = mPtr.getClass(); @@ -850,10 +870,7 @@ bool CharacterController::updateWeaponState() { forcestateupdate = true; - // Shields/torches shouldn't be visible during spellcasting or hand-to-hand - // 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. - mAnimation->showCarriedLeft(weaptype != WeapType_Spell && weaptype != WeapType_HandToHand); + mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype)); std::string weapgroup; if(weaptype == WeapType_None) @@ -1818,4 +1835,9 @@ void CharacterController::determineAttackType() } } +bool CharacterController::isReadyToBlock() const +{ + return updateCarriedLeftVisible(mWeaponType); +} + } diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 3409efedfd..075db37beb 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -199,6 +199,8 @@ class CharacterController /// @param num if non-NULL, the chosen animation number will be written here std::string chooseRandomGroup (const std::string& prefix, int* num = NULL); + bool updateCarriedLeftVisible(WeaponType weaptype) const; + public: CharacterController(const MWWorld::Ptr &ptr, MWRender::Animation *anim); virtual ~CharacterController(); @@ -224,6 +226,8 @@ public: void forceStateUpdate(); AiState& getAiState() { return mAiState; } + + bool isReadyToBlock() const; }; void getWeaponGroup(WeaponType weaptype, std::string &group); diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index 9225a57999..4d484469c2 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -62,17 +62,10 @@ namespace MWMechanics || blockerStats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getMagnitude() > 0) return false; - // Don't block when in spellcasting state (shield is equipped, but not visible) - if (blockerStats.getDrawState() == DrawState_Spell) + if (!MWBase::Environment::get().getMechanicsManager()->isReadyToBlock(blocker)) return false; MWWorld::InventoryStore& inv = blocker.getClass().getInventoryStore(blocker); - - // Don't block when in hand-to-hand combat (shield is equipped, but not visible) - if (blockerStats.getDrawState() == DrawState_Weapon && - inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight) == inv.end()) - return false; - MWWorld::ContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if (shield == inv.end() || shield->getTypeName() != typeid(ESM::Armor).name()) return false; diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 1dbe6f950e..cd9f0d0f76 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -1352,4 +1352,9 @@ namespace MWMechanics stats.resurrect(); } } + + bool MechanicsManager::isReadyToBlock(const MWWorld::Ptr &ptr) const + { + return mActors.isReadyToBlock(ptr); + } } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 1eec26c8ac..489da75417 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -169,6 +169,8 @@ namespace MWMechanics virtual bool isAggressive (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, int bias=0, bool ignoreDistance=false); virtual void keepPlayerAlive(); + + virtual bool isReadyToBlock (const MWWorld::Ptr& ptr) const; }; } diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 92d0bcd557..66052a96ec 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -195,6 +195,7 @@ namespace MWRender MWWorld::InventoryStore &inv = mCharacter.getClass().getInventoryStore(mCharacter); MWWorld::ContainerStoreIterator iter = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); std::string groupname; + bool showCarriedLeft = true; if(iter == inv.end()) groupname = "inventoryhandtohand"; else @@ -224,11 +225,15 @@ namespace MWRender groupname = "inventoryweapontwowide"; else groupname = "inventoryhandtohand"; - } + + showCarriedLeft = (iter->getClass().canBeEquipped(*iter, mCharacter).first != 2); + } else groupname = "inventoryhandtohand"; } + mAnimation->showCarriedLeft(showCarriedLeft); + mCurrentAnimGroup = groupname; mAnimation->play(mCurrentAnimGroup, 1, Animation::Group_All, false, 1.0f, "start", "stop", 0.0f, 0); diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index 6631c8f412..aba01bcfaa 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -143,7 +143,7 @@ public: virtual void setPitchFactor(float factor) { mPitchFactor = factor; } virtual void showWeapons(bool showWeapon); - virtual void showCarriedLeft(bool showa); + virtual void showCarriedLeft(bool show); virtual void attachArrow(); virtual void releaseArrow(); diff --git a/apps/openmw/mwworld/actionequip.cpp b/apps/openmw/mwworld/actionequip.cpp index 50da1e5e5d..87a4c63084 100644 --- a/apps/openmw/mwworld/actionequip.cpp +++ b/apps/openmw/mwworld/actionequip.cpp @@ -31,11 +31,7 @@ namespace MWWorld { case 0: return; - case 2: - invStore.unequipSlot(MWWorld::InventoryStore::Slot_CarriedLeft, actor); - break; - case 3: - invStore.unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight, actor); + default: break; } diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index 9032b04e11..c577d4b0d2 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -255,11 +255,7 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) { case 0: continue; - case 2: - slots_[MWWorld::InventoryStore::Slot_CarriedLeft] = end(); - break; - case 3: - // Prefer keeping twohanded weapon + default: break; } From 60aa20914411c96ea13d8949dda041a269d70036 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 12 Dec 2014 17:39:00 +0100 Subject: [PATCH 250/303] Implement drowning when knocked out underwater (Fixes #1228) --- apps/openmw/mwmechanics/actors.cpp | 14 ++++++++++---- apps/openmw/mwmechanics/character.cpp | 5 +++++ apps/openmw/mwmechanics/character.hpp | 1 + 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index a64b6c57d4..0730a4660f 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -869,13 +869,19 @@ namespace MWMechanics void Actors::updateDrowning(const MWWorld::Ptr& ptr, float duration) { - MWBase::World *world = MWBase::Environment::get().getWorld(); + PtrControllerMap::iterator it = mActors.find(ptr); + if (it == mActors.end()) + return; + CharacterController* ctrl = it->second; + NpcStats &stats = ptr.getClass().getNpcStats(ptr); - if(world->isSubmerged(ptr) && - stats.getMagicEffects().get(ESM::MagicEffect::WaterBreathing).getMagnitude() == 0) + MWBase::World *world = MWBase::Environment::get().getWorld(); + bool knockedOutUnderwater = (ctrl->isKnockedOut() && world->isUnderwater(ptr.getCell(), Ogre::Vector3(ptr.getRefData().getPosition().pos))); + if((world->isSubmerged(ptr) || knockedOutUnderwater) + && stats.getMagicEffects().get(ESM::MagicEffect::WaterBreathing).getMagnitude() == 0) { float timeLeft = 0.0f; - if(stats.getFatigue().getCurrent() == 0) + if(knockedOutUnderwater) stats.setTimeToStartDrowning(0); else { diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 06450ddb3c..d8693fbcdf 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1840,4 +1840,9 @@ bool CharacterController::isReadyToBlock() const return updateCarriedLeftVisible(mWeaponType); } +bool CharacterController::isKnockedOut() const +{ + return mHitState == CharState_KnockOut; +} + } diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 075db37beb..d7028a8441 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -228,6 +228,7 @@ public: AiState& getAiState() { return mAiState; } bool isReadyToBlock() const; + bool isKnockedOut() const; }; void getWeaponGroup(WeaponType weaptype, std::string &group); From ed6face4aa8cf6f1ae4ffc5aa18f0932199ff373 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 12 Dec 2014 22:21:57 +0100 Subject: [PATCH 251/303] Disable activation scripts for actors in combat --- apps/openmw/engine.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index d7b23c0966..15d63eb4b4 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -505,6 +505,9 @@ void OMW::Engine::activate() if (ptr.getClass().getName(ptr) == "") // objects without name presented to user can never be activated return; + if (ptr.getClass().isActor() && ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat()) + return; + MWBase::Environment::get().getWorld()->activate(ptr, MWBase::Environment::get().getWorld()->getPlayerPtr()); } From 2ebf328dec5b7748a0dd92478c851fb85803cbf9 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 13 Dec 2014 00:39:24 +0100 Subject: [PATCH 252/303] Always print the failing dialogue script These aren't usually very long, so printing them shouldn't spam the console by too much. --- apps/openmw/mwdialogue/dialoguemanagerimp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index eff54fbc01..4aab5003fd 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -227,7 +227,7 @@ namespace MWDialogue success = false; } - if (!success && mScriptVerbose) + if (!success) { std::cerr << "compiling failed (dialogue script)" << std::endl From 0ca11eab1c50dfa7a85c4aad46d213a10e0cc739 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 13 Dec 2014 02:39:56 +0100 Subject: [PATCH 253/303] Ignore extra argument for removeItem (Fixes #2208) --- components/compiler/extensions0.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index 1e2aebd991..690e589f73 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -116,7 +116,7 @@ namespace Compiler extensions.registerInstruction ("additem", "clX", opcodeAddItem, opcodeAddItemExplicit); extensions.registerFunction ("getitemcount", 'l', "c", opcodeGetItemCount, opcodeGetItemCountExplicit); - extensions.registerInstruction ("removeitem", "cl", opcodeRemoveItem, + extensions.registerInstruction ("removeitem", "clX", opcodeRemoveItem, opcodeRemoveItemExplicit); extensions.registerInstruction ("equip", "cX", opcodeEquip, opcodeEquipExplicit); extensions.registerFunction ("getarmortype", 'l', "l", opcodeGetArmorType, opcodeGetArmorTypeExplicit); From ba65c6cc7f19ed480a6dfe847b81593fc64d11b3 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 13 Dec 2014 02:47:04 +0100 Subject: [PATCH 254/303] Add --script-all-dialogue switch to compile all dialogue scripts (Fixes #1659) --- apps/openmw/CMakeLists.txt | 2 +- apps/openmw/engine.cpp | 18 ++- apps/openmw/engine.hpp | 4 + apps/openmw/main.cpp | 4 + apps/openmw/mwdialogue/dialoguemanagerimp.cpp | 2 +- apps/openmw/mwdialogue/filter.cpp | 11 ++ apps/openmw/mwdialogue/filter.hpp | 6 +- apps/openmw/mwdialogue/scripttest.cpp | 124 ++++++++++++++++++ apps/openmw/mwdialogue/scripttest.hpp | 20 +++ apps/openmw/mwscript/compilercontext.hpp | 2 +- 10 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 apps/openmw/mwdialogue/scripttest.cpp create mode 100644 apps/openmw/mwdialogue/scripttest.hpp diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 803c743259..06a142f0a1 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -44,7 +44,7 @@ add_openmw_dir (mwgui ) add_openmw_dir (mwdialogue - dialoguemanagerimp journalimp journalentry quest topic filter selectwrapper hypertextparser keywordsearch + dialoguemanagerimp journalimp journalentry quest topic filter selectwrapper hypertextparser keywordsearch scripttest ) add_openmw_dir (mwscript diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index d7b23c0966..1b350d7522 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -40,6 +40,7 @@ #include "mwdialogue/dialoguemanagerimp.hpp" #include "mwdialogue/journalimp.hpp" +#include "mwdialogue/scripttest.hpp" #include "mwmechanics/mechanicsmanagerimp.hpp" @@ -174,6 +175,7 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) , mSkipMenu (false) , mUseSound (true) , mCompileAll (false) + , mCompileAllDialogue (false) , mWarningsMode (1) , mScriptContext (0) , mFSStrict (false) @@ -425,7 +427,6 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) if (mCompileAll) { std::pair result = MWBase::Environment::get().getScriptManager()->compileAll(); - if (result.first) std::cout << "compiled " << result.second << " of " << result.first << " scripts (" @@ -433,6 +434,16 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) << "%)" << std::endl; } + if (mCompileAllDialogue) + { + std::pair result = MWDialogue::ScriptTest::compileAll(&mExtensions); + if (result.first) + std::cout + << "compiled " << result.second << " of " << result.first << " dialogue script/actor combinations a(" + << 100*static_cast (result.second)/result.first + << "%)" + << std::endl; + } } // Initialise and enter main loop. @@ -535,6 +546,11 @@ void OMW::Engine::setCompileAll (bool all) mCompileAll = all; } +void OMW::Engine::setCompileAllDialogue (bool all) +{ + mCompileAllDialogue = all; +} + void OMW::Engine::setSoundUsage(bool soundUsage) { mUseSound = soundUsage; diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 0ee5a09c5b..6cf31cba89 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -76,6 +76,7 @@ namespace OMW bool mSkipMenu; bool mUseSound; bool mCompileAll; + bool mCompileAllDialogue; int mWarningsMode; std::string mFocusName; std::map mFallbackMap; @@ -178,6 +179,9 @@ namespace OMW /// 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); diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index 744780b258..9382e2516b 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -130,6 +130,9 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat ("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") @@ -264,6 +267,7 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat // scripts engine.setCompileAll(variables["script-all"].as()); + engine.setCompileAllDialogue(variables["script-all-dialogue"].as()); engine.setScriptsVerbosity(variables["script-verbose"].as()); engine.setScriptConsoleMode (variables["script-console"].as()); engine.setStartupScript (variables["script-run"].as()); diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index eff54fbc01..196df00393 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -47,7 +47,7 @@ namespace MWDialogue { DialogueManager::DialogueManager (const Compiler::Extensions& extensions, bool scriptVerbose, Translation::Storage& translationDataStorage) : - mCompilerContext (MWScript::CompilerContext::Type_Dialgoue), + mCompilerContext (MWScript::CompilerContext::Type_Dialogue), mErrorStream(std::cout.rdbuf()),mErrorHandler(mErrorStream) , mTemporaryDispositionChange(0.f) , mPermanentDispositionChange(0.f), mScriptVerbose (scriptVerbose) diff --git a/apps/openmw/mwdialogue/filter.cpp b/apps/openmw/mwdialogue/filter.cpp index 629d99cc2a..af8a5754ff 100644 --- a/apps/openmw/mwdialogue/filter.cpp +++ b/apps/openmw/mwdialogue/filter.cpp @@ -603,6 +603,17 @@ const ESM::DialInfo* MWDialogue::Filter::search (const ESM::Dialogue& dialogue, 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 { diff --git a/apps/openmw/mwdialogue/filter.hpp b/apps/openmw/mwdialogue/filter.hpp index 7e7f2b6f54..d41b7aa1ef 100644 --- a/apps/openmw/mwdialogue/filter.hpp +++ b/apps/openmw/mwdialogue/filter.hpp @@ -55,7 +55,11 @@ namespace MWDialogue std::vector list (const ESM::Dialogue& dialogue, bool fallbackToInfoRefusal, bool searchAll, bool invertDisposition=false) const; - ///< \note If fallbackToInfoRefusal is used, the returned DialInfo might not be from the supplied ESM::Dialogue. + ///< 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. diff --git a/apps/openmw/mwdialogue/scripttest.cpp b/apps/openmw/mwdialogue/scripttest.cpp new file mode 100644 index 0000000000..a8de21ef97 --- /dev/null +++ b/apps/openmw/mwdialogue/scripttest.cpp @@ -0,0 +1,124 @@ +#include "scripttest.hpp" + +#include "../mwworld/manualref.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) +{ + MWDialogue::Filter filter(actor, 0, false); + + MWScript::CompilerContext compilerContext(MWScript::CompilerContext::Type_Dialogue); + compilerContext.setExtensions(extensions); + std::ostream errorStream(std::cout.rdbuf()); + Compiler::StreamErrorHandler errorHandler(errorStream); + + 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 it = infos.begin(); it != infos.end(); ++it) + { + const ESM::DialInfo* info = *it; + 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) + { + std::cerr << std::string ("Dialogue error: An exception has been thrown: ") + error.what() << std::endl; + success = false; + } + + if (!success) + { + std::cerr + << "compiling failed (dialogue script)" << std::endl + << info->mResultScript + << std::endl << std::endl; + } + } + } + } +} + +} + +namespace MWDialogue +{ + +namespace ScriptTest +{ + + std::pair compileAll(const Compiler::Extensions *extensions) + { + 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); + } + + 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); + } + return std::make_pair(total, compiled); + } + +} + +} diff --git a/apps/openmw/mwdialogue/scripttest.hpp b/apps/openmw/mwdialogue/scripttest.hpp new file mode 100644 index 0000000000..1ed94c76a5 --- /dev/null +++ b/apps/openmw/mwdialogue/scripttest.hpp @@ -0,0 +1,20 @@ +#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); + +} + +} + +#endif diff --git a/apps/openmw/mwscript/compilercontext.hpp b/apps/openmw/mwscript/compilercontext.hpp index 95719ab692..010926f451 100644 --- a/apps/openmw/mwscript/compilercontext.hpp +++ b/apps/openmw/mwscript/compilercontext.hpp @@ -12,7 +12,7 @@ namespace MWScript enum Type { Type_Full, // global, local, targetted - Type_Dialgoue, + Type_Dialogue, Type_Console }; From e4f75267d0475b2f7e2b19c698d9b70e85f58e4e Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 13 Dec 2014 15:40:24 +0100 Subject: [PATCH 255/303] in case of arguments not separated with comma the fist token of the next argument was put back incorrectly --- components/compiler/exprparser.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/compiler/exprparser.cpp b/components/compiler/exprparser.cpp index 6dcca08df9..f7c3d3ecb5 100644 --- a/components/compiler/exprparser.cpp +++ b/components/compiler/exprparser.cpp @@ -660,7 +660,7 @@ namespace Compiler else { // no comma was used between arguments - scanner.putbackKeyword (code, loc); + scanner.putbackSpecial (code, loc); return false; } } From ed5387fb8c9e21aa115655e6d874b2461bc4f472 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 13 Dec 2014 15:43:40 +0100 Subject: [PATCH 256/303] replaced stay [ ignoring implementation with one that does not interfere with other workarounds (Fixes #2205) --- components/compiler/lineparser.cpp | 7 +++++++ components/compiler/scanner.cpp | 3 +-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/components/compiler/lineparser.cpp b/components/compiler/lineparser.cpp index dc19b9a4b0..37641b014b 100644 --- a/components/compiler/lineparser.cpp +++ b/components/compiler/lineparser.cpp @@ -489,6 +489,13 @@ namespace Compiler bool LineParser::parseSpecial (int code, const TokenLoc& loc, Scanner& scanner) { + if (mState==EndState && code==Scanner::S_open) + { + getErrorHandler().warning ("stray '[' or '(' at the end of the line (ignoring it)", + loc); + return true; + } + if (code==Scanner::S_newline && (mState==EndState || mState==BeginState || mState==PotentialEndState)) return false; diff --git a/components/compiler/scanner.cpp b/components/compiler/scanner.cpp index 203f27e6e8..59fae3ccdc 100644 --- a/components/compiler/scanner.cpp +++ b/components/compiler/scanner.cpp @@ -528,8 +528,7 @@ namespace Compiler bool Scanner::isWhitespace (char c) { - return c==' ' || c=='\t' - || c=='['; ///< \todo disable this when doing more strict compiling + return c==' ' || c=='\t'; } // constructor From 8f29f2667e4081dd15a4449a700b2dc33cec8747 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 13 Dec 2014 15:49:32 +0100 Subject: [PATCH 257/303] Fix rotation order for XYZ rotation keys (Fixes #1067) --- components/nifogre/ogrenifloader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/nifogre/ogrenifloader.cpp b/components/nifogre/ogrenifloader.cpp index 1d1e1a22d0..cdbc4d8447 100644 --- a/components/nifogre/ogrenifloader.cpp +++ b/components/nifogre/ogrenifloader.cpp @@ -483,7 +483,7 @@ public: Ogre::Quaternion xr(Ogre::Radian(xrot), Ogre::Vector3::UNIT_X); Ogre::Quaternion yr(Ogre::Radian(yrot), Ogre::Vector3::UNIT_Y); Ogre::Quaternion zr(Ogre::Radian(zrot), Ogre::Vector3::UNIT_Z); - return (xr*yr*zr); + return (zr*yr*xr); } public: From 80c92789c23d899dc78ef4fef850d54763854889 Mon Sep 17 00:00:00 2001 From: Nik Dyonin Date: Sun, 14 Dec 2014 02:58:42 +0300 Subject: [PATCH 258/303] Fix issue when killed NPC cannot be looted if it was in combat mode before killing. --- apps/openmw/engine.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 885d2bb4f1..d8546a2490 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -516,8 +516,13 @@ void OMW::Engine::activate() if (ptr.getClass().getName(ptr) == "") // objects without name presented to user can never be activated return; - if (ptr.getClass().isActor() && ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat()) - return; + if (ptr.getClass().isActor()) + { + MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); + + if (stats.getAiSequence().isInCombat() && !stats.isDead()) + return; + } MWBase::Environment::get().getWorld()->activate(ptr, MWBase::Environment::get().getWorld()->getPlayerPtr()); } From 88a2e4c04383dfe7c2c3652f6995456564b4f1cd Mon Sep 17 00:00:00 2001 From: scrawl Date: Sun, 14 Dec 2014 01:53:15 +0100 Subject: [PATCH 259/303] Graceful error handling for missing spells/factions (Fixes #1825, Bug #2176, Bug #2203) --- apps/openmw/mwclass/creature.cpp | 8 ++++++- apps/openmw/mwclass/npc.cpp | 34 +++++++++++++++++++++++------- apps/openmw/mwmechanics/spells.cpp | 16 ++++++++------ apps/openmw/mwmechanics/spells.hpp | 3 +++ 4 files changed, 46 insertions(+), 15 deletions(-) diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index d3c216c2b2..25808c991a 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -119,7 +119,13 @@ namespace MWClass // spells for (std::vector::const_iterator iter (ref->mBase->mSpells.mList.begin()); iter!=ref->mBase->mSpells.mList.end(); ++iter) - data->mCreatureStats.getSpells().add (*iter); + { + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(*iter); + if (spell) + data->mCreatureStats.getSpells().add (spell); + else /// \todo add option to make this a fatal error message pop-up, but default to warning for vanilla compatibility + std::cerr << "Warning: ignoring nonexistent spell '" << spell->mId << "' on creature '" << ref->mBase->mId << "'" << std::endl; + } // inventory if (ref->mBase->mFlags & ESM::Creature::Weapon) diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 6806558833..3da7446918 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -300,15 +300,20 @@ namespace MWClass if (!ref->mBase->mFaction.empty()) { std::string faction = ref->mBase->mFaction; - Misc::StringUtils::toLower(faction); - if(ref->mBase->mNpdtType != ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) + const ESM::Faction* fact = MWBase::Environment::get().getWorld()->getStore().get().search(faction); + if (fact) { - data->mNpcStats.setFactionRank(faction, (int)ref->mBase->mNpdt52.mRank); + if(ref->mBase->mNpdtType != ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) + { + data->mNpcStats.setFactionRank(fact->mId, (int)ref->mBase->mNpdt52.mRank); + } + else + { + data->mNpcStats.setFactionRank(fact->mId, (int)ref->mBase->mNpdt12.mRank); + } } else - { - data->mNpcStats.setFactionRank(faction, (int)ref->mBase->mNpdt12.mRank); - } + std::cerr << "Warning: ignoring nonexistent faction '" << fact->mId << "' on NPC '" << ref->mBase->mId << "'" << std::endl; } // creature stats @@ -361,7 +366,11 @@ namespace MWClass for (std::vector::const_iterator iter (race->mPowers.mList.begin()); iter!=race->mPowers.mList.end(); ++iter) { - data->mNpcStats.getSpells().add (*iter); + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(*iter); + if (spell) + data->mNpcStats.getSpells().add (spell); + else + std::cerr << "Warning: ignoring nonexistent race power '" << *iter << "' on NPC '" << ref->mBase->mId << "'" << std::endl; } if (data->mNpcStats.getFactionRanks().size()) @@ -385,7 +394,16 @@ namespace MWClass // spells for (std::vector::const_iterator iter (ref->mBase->mSpells.mList.begin()); iter!=ref->mBase->mSpells.mList.end(); ++iter) - data->mNpcStats.getSpells().add (*iter); + { + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(*iter); + if (spell) + data->mNpcStats.getSpells().add (spell); + else + { + /// \todo add option to make this a fatal error message pop-up, but default to warning for vanilla compatibility + std::cerr << "Warning: ignoring nonexistent spell '" << *iter << "' on NPC '" << ref->mBase->mId << "'" << std::endl; + } + } // inventory data->mInventoryStore.fill(ref->mBase->mInventory, getId(ptr), "", diff --git a/apps/openmw/mwmechanics/spells.cpp b/apps/openmw/mwmechanics/spells.cpp index a1b73bc47c..5953be523b 100644 --- a/apps/openmw/mwmechanics/spells.cpp +++ b/apps/openmw/mwmechanics/spells.cpp @@ -25,12 +25,10 @@ namespace MWMechanics return mSpells.end(); } - void Spells::add (const std::string& spellId) + void Spells::add (const ESM::Spell* spell) { - if (mSpells.find (spellId)==mSpells.end()) + if (mSpells.find (spell->mId)==mSpells.end()) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellId); - std::map random; // Determine the random magnitudes (unless this is a castable spell, in which case @@ -50,13 +48,19 @@ namespace MWMechanics corprus.mWorsenings = 0; corprus.mNextWorsening = MWBase::Environment::get().getWorld()->getTimeStamp() + CorprusStats::sWorseningPeriod; - mCorprusSpells[spellId] = corprus; + mCorprusSpells[spell->mId] = corprus; } - mSpells.insert (std::make_pair (Misc::StringUtils::lowerCase(spellId), random)); + mSpells.insert (std::make_pair (spell->mId, random)); } } + void Spells::add (const std::string& spellId) + { + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellId); + add(spell); + } + void Spells::remove (const std::string& spellId) { std::string lower = Misc::StringUtils::lowerCase(spellId); diff --git a/apps/openmw/mwmechanics/spells.hpp b/apps/openmw/mwmechanics/spells.hpp index ab799ffe12..064b2c1e5d 100644 --- a/apps/openmw/mwmechanics/spells.hpp +++ b/apps/openmw/mwmechanics/spells.hpp @@ -79,6 +79,9 @@ namespace MWMechanics 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). From 97a4b30b59c232199a87c8b97dd00fb824fd4f20 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 14 Dec 2014 14:59:27 +0100 Subject: [PATCH 260/303] updated credits file --- credits.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/credits.txt b/credits.txt index 76efb2760a..c3eab721fe 100644 --- a/credits.txt +++ b/credits.txt @@ -71,6 +71,7 @@ Michael Papageorgiou (werdanith) Michał Bień (Glorf) Miroslav Puda (pakanek) MiroslavR +Narmo Nathan Jeffords (blunted2night) Nikolay Kasyanov (corristo) nobrakal From c5a604453ec71b3d2d366202ff02ec93f7ce304b Mon Sep 17 00:00:00 2001 From: MiroslavR Date: Sun, 14 Dec 2014 16:25:27 +0100 Subject: [PATCH 261/303] Fix several book formatting issues (Fixes #2204) --- apps/openmw/mwgui/formatting.cpp | 33 ++++++++++++++++++++++++++++---- apps/openmw/mwgui/formatting.hpp | 6 +++++- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwgui/formatting.cpp b/apps/openmw/mwgui/formatting.cpp index c55650c453..5831160034 100644 --- a/apps/openmw/mwgui/formatting.cpp +++ b/apps/openmw/mwgui/formatting.cpp @@ -193,6 +193,9 @@ namespace MWGui 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); @@ -207,8 +210,25 @@ namespace MWGui 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 = false; + brBeforeLastTag = true; else { // Each block of text (between two tags / boundary and tag) will be displayed in a separate editbox widget, @@ -252,6 +272,8 @@ namespace MWGui { case BookTextParser::Event_ImgTag: { + pag.setIgnoreLeadingEmptyLines(false); + const BookTextParser::Attributes & attr = parser.getAttributes(); if (attr.find("src") == attr.end() || attr.find("width") == attr.end() || attr.find("height") == attr.end()) @@ -331,9 +353,7 @@ namespace MWGui if (attr.find("face") != attr.end()) { std::string face = attr.at("face"); - - if (face != "Magic Cards") - mTextStyle.mFont = face; + mTextStyle.mFont = face; } if (attr.find("size") != attr.end()) { @@ -408,13 +428,18 @@ namespace MWGui // first empty lines that would go to the next page should be ignored // unfortunately, getLineInfo method won't be available until 3.2.2 #if (MYGUI_VERSION >= MYGUI_DEFINE_VERSION(3, 2, 2)) + 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; + } } #endif return ret; diff --git a/apps/openmw/mwgui/formatting.hpp b/apps/openmw/mwgui/formatting.hpp index 5b79250577..0d0f74b720 100644 --- a/apps/openmw/mwgui/formatting.hpp +++ b/apps/openmw/mwgui/formatting.hpp @@ -81,7 +81,8 @@ namespace MWGui Paginator(int pageWidth, int pageHeight) : mStartTop(0), mCurrentTop(0), - mPageWidth(pageWidth), mPageHeight(pageHeight) + mPageWidth(pageWidth), mPageHeight(pageHeight), + mIgnoreLeadingEmptyLines(false) { } @@ -89,10 +90,12 @@ namespace MWGui 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) { @@ -103,6 +106,7 @@ namespace MWGui private: int mStartTop, mCurrentTop; int mPageWidth, mPageHeight; + bool mIgnoreLeadingEmptyLines; Pages mPages; }; From 192626c6f55421d57f20d4c963efde17dc95b0a1 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sun, 14 Dec 2014 17:44:03 +0100 Subject: [PATCH 262/303] SoundGen fix: use Original Creature field only if non-empty --- apps/openmw/mwclass/creature.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 25808c991a..b87dfda98b 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -679,14 +679,15 @@ namespace MWClass if(type >= 0) { std::vector sounds; - sounds.reserve(8); MWWorld::LiveCellRef* ref = ptr.get(); + const std::string& ourId = (ref->mBase->mOriginal.empty()) ? getId(ptr) : ref->mBase->mOriginal; + MWWorld::Store::iterator sound = store.begin(); while(sound != store.end()) { - if (type == sound->mType && !sound->mCreature.empty() && Misc::StringUtils::ciEqual(ref->mBase->mOriginal, sound->mCreature)) + if (type == sound->mType && !sound->mCreature.empty() && (Misc::StringUtils::ciEqual(ourId, sound->mCreature))) sounds.push_back(&*sound); ++sound; } From 4acc25f59c65101257ad3144e8935f2e345881b7 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sun, 14 Dec 2014 17:52:06 +0100 Subject: [PATCH 263/303] Use SoundGen with no creature field as fallback This fixes the adorable "thump" sounds in the Scrib's idle animation not playing. --- apps/openmw/mwclass/creature.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index b87dfda98b..1b005270f2 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -679,6 +679,7 @@ namespace MWClass if(type >= 0) { std::vector sounds; + std::vector fallbacksounds; MWWorld::LiveCellRef* ref = ptr.get(); @@ -689,10 +690,14 @@ namespace MWClass { 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()) return sounds[(int)(rand()/(RAND_MAX+1.0)*sounds.size())]->mSound; + if (!fallbacksounds.empty()) + return fallbacksounds[(int)(rand()/(RAND_MAX+1.0)*fallbacksounds.size())]->mSound; } return ""; From 4f3995a4d8f407557bedede17cbf9069fb707ee6 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sun, 14 Dec 2014 19:15:43 +0100 Subject: [PATCH 264/303] Fix werewolf AI being able to use items --- apps/openmw/mwmechanics/aicombataction.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index 6b4ede3059..73cb276262 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -454,6 +454,8 @@ namespace MWMechanics float bestActionRating = 0.f; // Default to hand-to-hand combat boost::shared_ptr bestAction (new ActionWeapon(MWWorld::Ptr())); + if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) + return bestAction; if (actor.getClass().hasInventoryStore(actor)) { From 2b78e9795d346e2d54f6833baba258d7b2a1158c Mon Sep 17 00:00:00 2001 From: scrawl Date: Sun, 14 Dec 2014 19:35:34 +0100 Subject: [PATCH 265/303] Implement Calm effect removing combat packages (Fixes #1985) --- apps/openmw/mwclass/creature.cpp | 6 ++++++ apps/openmw/mwclass/creature.hpp | 2 ++ apps/openmw/mwclass/npc.cpp | 7 +++++++ apps/openmw/mwclass/npc.hpp | 2 ++ apps/openmw/mwmechanics/actors.cpp | 14 ++++++++++++++ apps/openmw/mwmechanics/aicombataction.cpp | 3 +++ apps/openmw/mwmechanics/aisequence.cpp | 5 ++--- apps/openmw/mwmechanics/aisequence.hpp | 2 +- apps/openmw/mwmechanics/mechanicsmanagerimp.cpp | 5 +++++ apps/openmw/mwworld/class.cpp | 5 +++++ apps/openmw/mwworld/class.hpp | 2 ++ 11 files changed, 49 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 1b005270f2..519216ec9b 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -891,4 +891,10 @@ namespace MWClass MWWorld::ContainerStore& store = getContainerStore(ptr); store.restock(list, ptr, ptr.getCellRef().getRefId(), ptr.getCellRef().getFaction()); } + + int Creature::getBaseFightRating(const MWWorld::Ptr &ptr) const + { + MWWorld::LiveCellRef *ref = ptr.get(); + return ref->mBase->mAiData.mFight; + } } diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp index 1820d4ea43..c52e85534b 100644 --- a/apps/openmw/mwclass/creature.hpp +++ b/apps/openmw/mwclass/creature.hpp @@ -154,6 +154,8 @@ namespace MWClass virtual void respawn (const MWWorld::Ptr& ptr) const; virtual void restock (const MWWorld::Ptr &ptr) const; + + virtual int getBaseFightRating(const MWWorld::Ptr &ptr) const; }; } diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 3da7446918..a3ebef5821 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -1266,6 +1266,7 @@ namespace MWClass // 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 ""; if(name == "roar") @@ -1380,4 +1381,10 @@ namespace MWClass MWWorld::ContainerStore& store = getContainerStore(ptr); store.restock(list, ptr, ptr.getCellRef().getRefId(), ptr.getCellRef().getFaction()); } + + int Npc::getBaseFightRating (const MWWorld::Ptr& ptr) const + { + MWWorld::LiveCellRef *ref = ptr.get(); + return ref->mBase->mAiData.mFight; + } } diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp index a92e72af59..3bc450088b 100644 --- a/apps/openmw/mwclass/npc.hpp +++ b/apps/openmw/mwclass/npc.hpp @@ -185,6 +185,8 @@ namespace MWClass virtual void respawn (const MWWorld::Ptr& ptr) const; virtual void restock (const MWWorld::Ptr& ptr) const; + + virtual int getBaseFightRating (const MWWorld::Ptr& ptr) const; }; } diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 0730a4660f..fecf189861 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -677,6 +677,19 @@ namespace MWMechanics // TODO: dirty flag for magic effects to avoid some unnecessary work below? + // any value of calm > 0 will stop the actor from fighting + if ((creatureStats.getMagicEffects().get(ESM::MagicEffect::CalmHumanoid).getMagnitude() > 0 && ptr.getClass().isNpc()) + || (creatureStats.getMagicEffects().get(ESM::MagicEffect::CalmCreature).getMagnitude() > 0 && !ptr.getClass().isNpc())) + { + for (std::list::const_iterator it = creatureStats.getAiSequence().begin(); it != creatureStats.getAiSequence().end(); ) + { + if ((*it)->getTypeId() == AiPackage::TypeIdCombat) + it = creatureStats.getAiSequence().erase(it); + else + ++it; + } + } + // Update bound effects static std::map boundItemsMap; if (boundItemsMap.empty()) @@ -1056,6 +1069,7 @@ namespace MWMechanics // 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); diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index 73cb276262..356955b221 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -455,7 +455,10 @@ namespace MWMechanics // Default to hand-to-hand combat boost::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)) { diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index 990145c8d5..2ee8984050 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -86,15 +86,14 @@ std::list::const_iterator AiSequence::end() const return mPackages.end(); } -void AiSequence::erase(std::list::const_iterator package) +std::list::const_iterator AiSequence::erase(std::list::const_iterator package) { // Not sure if manually terminated packages should trigger mDone, probably not? for(std::list::iterator it = mPackages.begin(); it != mPackages.end(); ++it) { if (package == it) { - mPackages.erase(it); - return; + return mPackages.erase(it); } } throw std::runtime_error("can't find package to erase"); diff --git a/apps/openmw/mwmechanics/aisequence.hpp b/apps/openmw/mwmechanics/aisequence.hpp index 25605ff447..460a411ba8 100644 --- a/apps/openmw/mwmechanics/aisequence.hpp +++ b/apps/openmw/mwmechanics/aisequence.hpp @@ -61,7 +61,7 @@ namespace MWMechanics std::list::const_iterator begin() const; std::list::const_iterator end() const; - void erase (std::list::const_iterator package); + std::list::const_iterator erase (std::list::const_iterator package); /// Returns currently executing AiPackage type /** \see enum AiPackage::TypeId **/ diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index cd9f0d0f76..520b6b8a7c 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -1096,6 +1096,11 @@ namespace MWMechanics { startCombat(*it, player); + // Apply aggression value to the base Fight rating, so that the actor can continue fighting + // after a Calm spell wears off + int fightBase = it->getClass().getCreatureStats(*it).getAiSetting(CreatureStats::AI_Fight).getBase(); + it->getClass().getCreatureStats(*it).setAiSetting(CreatureStats::AI_Fight, fightBase + aggression); + // Set the crime ID, which we will use to calm down participants // once the bounty has been paid. it->getClass().getNpcStats(*it).setCrimeId(id); diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index 0a84862097..7c95858343 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -429,4 +429,9 @@ namespace MWWorld { return std::string(); } + + int Class::getBaseFightRating(const Ptr &ptr) const + { + throw std::runtime_error("class does not support fight rating"); + } } diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index dcac16b06e..cc18d830cc 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -339,6 +339,8 @@ namespace MWWorld /// Returns sound id virtual std::string getSound(const MWWorld::Ptr& ptr) const; + + virtual int getBaseFightRating (const MWWorld::Ptr& ptr) const; }; } From 79237d16a7502666e06d963384da759d2800e69d Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 15 Dec 2014 13:13:25 +0100 Subject: [PATCH 266/303] Refactor spell window to use model/view and remove duplicated code in QuickKeysMenu This should also improve window resizing performance, the widgets are now just resized instead of recreated. --- apps/openmw/CMakeLists.txt | 2 +- apps/openmw/mwgui/quickkeysmenu.cpp | 218 +----------- apps/openmw/mwgui/quickkeysmenu.hpp | 16 +- apps/openmw/mwgui/spellmodel.cpp | 140 ++++++++ apps/openmw/mwgui/spellmodel.hpp | 56 +++ apps/openmw/mwgui/spellview.cpp | 255 ++++++++++++++ apps/openmw/mwgui/spellview.hpp | 71 ++++ apps/openmw/mwgui/spellwindow.cpp | 321 ++---------------- apps/openmw/mwgui/spellwindow.hpp | 25 +- apps/openmw/mwgui/windowmanagerimp.cpp | 2 + files/mygui/openmw_list.skin.xml | 9 + .../mygui/openmw_magicselection_dialog.layout | 6 +- files/mygui/openmw_spell_window.layout | 5 +- 13 files changed, 585 insertions(+), 541 deletions(-) create mode 100644 apps/openmw/mwgui/spellmodel.cpp create mode 100644 apps/openmw/mwgui/spellmodel.hpp create mode 100644 apps/openmw/mwgui/spellview.cpp create mode 100644 apps/openmw/mwgui/spellview.hpp diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 06a142f0a1..28eadc5172 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -40,7 +40,7 @@ add_openmw_dir (mwgui merchantrepair repair soulgemdialog companionwindow bookpage journalviewmodel journalbooks itemmodel containeritemmodel inventoryitemmodel sortfilteritemmodel itemview tradeitemmodel companionitemmodel pickpocketitemmodel controllers savegamedialog - recharge mode videowidget backgroundimage itemwidget screenfader debugwindow + recharge mode videowidget backgroundimage itemwidget screenfader debugwindow spellmodel spellview ) add_openmw_dir (mwdialogue diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 440165e749..7ffb51b5ab 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -13,7 +13,6 @@ #include "../mwbase/world.hpp" #include "../mwmechanics/spellcasting.hpp" -#include "../mwmechanics/spells.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwgui/inventorywindow.hpp" @@ -23,7 +22,8 @@ #include "windowmanagerimp.hpp" #include "itemselection.hpp" -#include "spellwindow.hpp" +#include "spellview.hpp" + #include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" @@ -495,13 +495,15 @@ namespace MWGui MagicSelectionDialog::MagicSelectionDialog(QuickKeysMenu* parent) : WindowModal("openmw_magicselection_dialog.layout") , mParent(parent) - , mWidth(0) - , mHeight(0) { 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(); } @@ -519,211 +521,17 @@ namespace MWGui { WindowModal::open(); - while (mMagicList->getChildCount ()) - MyGUI::Gui::getInstance ().destroyWidget (mMagicList->getChildAt (0)); - - mHeight = 0; - - const int spellHeight = 18; - - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); - MWMechanics::Spells& spells = stats.getSpells(); - - /// \todo lots of copy&pasted code from SpellWindow - - // retrieve powers & spells, sort by name - std::vector spellList; - - for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) - { - spellList.push_back (it->first); - } - - const MWWorld::ESMStore &esmStore = - MWBase::Environment::get().getWorld()->getStore(); - - std::vector powers; - std::vector::iterator it = spellList.begin(); - while (it != spellList.end()) - { - const ESM::Spell* spell = esmStore.get().find(*it); - if (spell->mData.mType == ESM::Spell::ST_Power) - { - powers.push_back(*it); - it = spellList.erase(it); - } - else if (spell->mData.mType == ESM::Spell::ST_Ability - || spell->mData.mType == ESM::Spell::ST_Blight - || spell->mData.mType == ESM::Spell::ST_Curse - || spell->mData.mType == ESM::Spell::ST_Disease) - { - it = spellList.erase(it); - } - else - ++it; - } - std::sort(powers.begin(), powers.end(), sortSpells); - std::sort(spellList.begin(), spellList.end(), sortSpells); - - // retrieve usable magic items & sort - std::vector items; - for (MWWorld::ContainerStoreIterator it(store.begin()); it != store.end(); ++it) - { - std::string enchantId = it->getClass().getEnchantment(*it); - if (enchantId != "") - { - // only add items with "Cast once" or "Cast on use" - const ESM::Enchantment* enchant = - esmStore.get().find(enchantId); - - int type = enchant->mData.mType; - if (type != ESM::Enchantment::CastOnce - && type != ESM::Enchantment::WhenUsed) - continue; - - items.push_back(*it); - } - } - std::sort(items.begin(), items.end(), sortItems); - - - int height = estimateHeight(items.size() + powers.size() + spellList.size()); - bool scrollVisible = height > mMagicList->getHeight(); - mWidth = mMagicList->getWidth() - scrollVisible * 18; - - - // powers - addGroup("#{sPowers}", ""); - - for (std::vector::const_iterator it = powers.begin(); it != powers.end(); ++it) - { - const ESM::Spell* spell = esmStore.get().find(*it); - MyGUI::Button* t = mMagicList->createWidget("SandTextButton", - MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); - t->setCaption(spell->mName); - t->setTextAlign(MyGUI::Align::Left); - t->setUserString("ToolTipType", "Spell"); - t->setUserString("Spell", *it); - t->eventMouseWheel += MyGUI::newDelegate(this, &MagicSelectionDialog::onMouseWheel); - t->eventMouseButtonClick += MyGUI::newDelegate(this, &MagicSelectionDialog::onSpellSelected); - - mHeight += spellHeight; - } - - // other spells - addGroup("#{sSpells}", ""); - for (std::vector::const_iterator it = spellList.begin(); it != spellList.end(); ++it) - { - const ESM::Spell* spell = esmStore.get().find(*it); - MyGUI::Button* t = mMagicList->createWidget("SandTextButton", - MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); - t->setCaption(spell->mName); - t->setTextAlign(MyGUI::Align::Left); - t->setUserString("ToolTipType", "Spell"); - t->setUserString("Spell", *it); - t->eventMouseWheel += MyGUI::newDelegate(this, &MagicSelectionDialog::onMouseWheel); - t->eventMouseButtonClick += MyGUI::newDelegate(this, &MagicSelectionDialog::onSpellSelected); - - mHeight += spellHeight; - } - - - // enchanted items - addGroup("#{sMagicItem}", ""); - - for (std::vector::const_iterator it = items.begin(); it != items.end(); ++it) - { - MWWorld::Ptr item = *it; - - // check if the item is currently equipped (will display in a different color) - bool equipped = false; - for (int i=0; i < MWWorld::InventoryStore::Slots; ++i) - { - if (store.getSlot(i) != store.end() && *store.getSlot(i) == item) - { - equipped = true; - break; - } - } - - MyGUI::Button* t = mMagicList->createWidget(equipped ? "SandTextButton" : "SpellTextUnequipped", - MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); - t->setCaption(item.getClass().getName(item)); - t->setTextAlign(MyGUI::Align::Left); - t->setUserData(item); - t->setUserString("ToolTipType", "ItemPtr"); - t->eventMouseButtonClick += MyGUI::newDelegate(this, &MagicSelectionDialog::onEnchantedItemSelected); - t->eventMouseWheel += MyGUI::newDelegate(this, &MagicSelectionDialog::onMouseWheel); - - mHeight += spellHeight; - } - - // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden - mMagicList->setVisibleVScroll(false); - mMagicList->setCanvasSize (mWidth, std::max(mMagicList->getHeight(), mHeight)); - mMagicList->setVisibleVScroll(true); + mMagicList->setModel(new SpellModel(MWBase::Environment::get().getWorld()->getPlayerPtr())); + mMagicList->update(); } - void MagicSelectionDialog::addGroup(const std::string &label, const std::string& label2) + void MagicSelectionDialog::onModelIndexSelected(SpellModel::ModelIndex index) { - if (mMagicList->getChildCount() > 0) - { - MyGUI::ImageBox* separator = mMagicList->createWidget("MW_HLine", - MyGUI::IntCoord(4, mHeight, mWidth-8, 18), - MyGUI::Align::Left | MyGUI::Align::Top); - separator->setNeedMouseFocus(false); - mHeight += 18; - } - - MyGUI::TextBox* groupWidget = mMagicList->createWidget("SandBrightText", - MyGUI::IntCoord(0, mHeight, mWidth, 24), - MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); - groupWidget->setCaptionWithReplacing(label); - groupWidget->setTextAlign(MyGUI::Align::Left); - groupWidget->setNeedMouseFocus(false); - - if (label2 != "") - { - MyGUI::TextBox* groupWidget2 = mMagicList->createWidget("SandBrightText", - MyGUI::IntCoord(0, mHeight, mWidth-4, 24), - MyGUI::Align::Left | MyGUI::Align::Top); - groupWidget2->setCaptionWithReplacing(label2); - groupWidget2->setTextAlign(MyGUI::Align::Right); - groupWidget2->setNeedMouseFocus(false); - } - - mHeight += 24; - } - - - void MagicSelectionDialog::onMouseWheel(MyGUI::Widget* _sender, int _rel) - { - if (mMagicList->getViewOffset().top + _rel*0.3 > 0) - mMagicList->setViewOffset(MyGUI::IntPoint(0, 0)); + const Spell& spell = mMagicList->getModel()->getItem(index); + if (spell.mType == Spell::Type_EnchantedItem) + mParent->onAssignMagicItem(spell.mItem); else - mMagicList->setViewOffset(MyGUI::IntPoint(0, mMagicList->getViewOffset().top + _rel*0.3)); - } - - void MagicSelectionDialog::onEnchantedItemSelected(MyGUI::Widget* _sender) - { - MWWorld::Ptr item = *_sender->getUserData(); - - mParent->onAssignMagicItem (item); - } - - void MagicSelectionDialog::onSpellSelected(MyGUI::Widget* _sender) - { - mParent->onAssignMagic (_sender->getUserString("Spell")); - } - - int MagicSelectionDialog::estimateHeight(int numSpells) const - { - int height = 0; - height += 24 * 3 + 18 * 2; // group headings - height += numSpells * 18; - return height; + mParent->onAssignMagic(spell.mId); } } diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index dc088d3c9a..30e6728911 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -5,6 +5,8 @@ #include "windowbase.hpp" +#include "spellmodel.hpp" + namespace MWGui { @@ -12,6 +14,7 @@ namespace MWGui class ItemSelectionDialog; class MagicSelectionDialog; class ItemWidget; + class SpellView; class QuickKeysMenu : public WindowBase { @@ -94,21 +97,12 @@ namespace MWGui private: MyGUI::Button* mCancelButton; - MyGUI::ScrollView* mMagicList; - - int mWidth; - int mHeight; + SpellView* mMagicList; QuickKeysMenu* mParent; void onCancelButtonClicked (MyGUI::Widget* sender); - void onMouseWheel(MyGUI::Widget* _sender, int _rel); - void onEnchantedItemSelected(MyGUI::Widget* _sender); - void onSpellSelected(MyGUI::Widget* _sender); - int estimateHeight(int numSpells) const; - - - void addGroup(const std::string& label, const std::string& label2); + void onModelIndexSelected(SpellModel::ModelIndex index); }; } diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp new file mode 100644 index 0000000000..0305258978 --- /dev/null +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -0,0 +1,140 @@ +#include "spellmodel.hpp" + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwbase/windowmanager.hpp" + +#include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/spellcasting.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; + + int cmp = left.mName.compare(right.mName); + return cmp < 0; + } + +} + +namespace MWGui +{ + + SpellModel::SpellModel(const MWWorld::Ptr &actor) + : mActor(actor) + { + + } + + 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(); + + for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + { + const ESM::Spell* spell = esmStore.get().find(it->first); + if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) + continue; + + Spell newSpell; + newSpell.mName = spell->mName; + if (spell->mData.mType == ESM::Spell::ST_Spell) + { + newSpell.mType = Spell::Type_Spell; + std::string cost = boost::lexical_cast(spell->mData.mCost); + std::string chance = boost::lexical_cast(int(MWMechanics::getSpellSuccessChance(spell, mActor))); + newSpell.mCostColumn = cost + "/" + chance; + } + else + newSpell.mType = Spell::Type_Power; + newSpell.mId = it->first; + + newSpell.mSelected = (MWBase::Environment::get().getWindowManager()->getSelectedSpell() == it->first); + newSpell.mActive = true; + 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().find(item.getClass().getEnchantment(item)); + if (enchant->mData.mType != ESM::Enchantment::WhenUsed && enchant->mData.mType != ESM::Enchantment::CastOnce) + continue; + + Spell newSpell; + newSpell.mItem = item; + newSpell.mId = item.getClass().getId(item); + newSpell.mName = item.getClass().getName(item); + 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 + { + float enchantCost = enchant->mData.mCost; + int eSkill = mActor.getClass().getSkill(mActor, ESM::Skill::Enchant); + int castCost = std::max(1.f, enchantCost - (enchantCost / 100) * (eSkill - 10)); + + std::string cost = boost::lexical_cast(castCost); + int currentCharge = int(item.getCellRef().getEnchantmentCharge()); + if (currentCharge == -1) + currentCharge = enchant->mData.mCharge; + std::string charge = boost::lexical_cast(currentCharge); + newSpell.mCostColumn = cost + "/" + charge; + + bool equipped = false; + for (int i=0; i < MWWorld::InventoryStore::Slots; ++i) + { + if (invStore.getSlot(i) != invStore.end() && *invStore.getSlot(i) == item) + { + equipped = true; + break; + } + } + newSpell.mActive = equipped; + } + mSpells.push_back(newSpell); + } + + std::stable_sort(mSpells.begin(), mSpells.end(), sortSpells); + } + + size_t SpellModel::getItemCount() const + { + return mSpells.size(); + } + + Spell SpellModel::getItem(ModelIndex index) const + { + if (index < 0 || index >= int(mSpells.size())) + throw std::runtime_error("invalid spell index supplied"); + return mSpells[index]; + } + +} diff --git a/apps/openmw/mwgui/spellmodel.hpp b/apps/openmw/mwgui/spellmodel.hpp new file mode 100644 index 0000000000..1f7a0cb7c9 --- /dev/null +++ b/apps/openmw/mwgui/spellmodel.hpp @@ -0,0 +1,56 @@ +#ifndef OPENMW_GUI_SPELLMODEL_H +#define OPENMW_GUI_SPELLMODEL_H + +#include "../mwworld/ptr.hpp" + +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 + 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() + : 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); + + typedef int ModelIndex; + + void update(); + + Spell getItem (ModelIndex index) const; + ///< throws for invalid index + + size_t getItemCount() const; + + private: + MWWorld::Ptr mActor; + + std::vector mSpells; + }; + +} + +#endif diff --git a/apps/openmw/mwgui/spellview.cpp b/apps/openmw/mwgui/spellview.cpp new file mode 100644 index 0000000000..3e5a01e3a7 --- /dev/null +++ b/apps/openmw/mwgui/spellview.cpp @@ -0,0 +1,255 @@ +#include "spellview.hpp" + +#include +#include +#include +#include + +#include + +namespace MWGui +{ + + SpellView::SpellView() + : mShowCostColumn(true) + , mHighlightSelected(true) + , mScrollView(NULL) + { + } + + void SpellView::initialiseOverride() + { + Base::initialiseOverride(); + + assignWidget(mScrollView, "ScrollView"); + if (mScrollView == NULL) + 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}", "#{sCostChance}"); + else + addGroup("#{sMagicItem}", "#{sCostCharge}"); + curType = spell.mType; + } + + const std::string skin = spell.mActive ? "SandTextButton" : "SpellTextUnequipped"; + + Gui::SharedStateButton* t = mScrollView->createWidget(skin, + MyGUI::IntCoord(0, 0, 0, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); + t->setCaption(spell.mName); + 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.push_back(std::make_pair(t, costChance)); + } + else + mLines.push_back(std::make_pair(t, (MyGUI::Widget*)NULL)); + + t->setStateSelected(spell.mSelected); + } + + layoutWidgets(); + } + + void SpellView::layoutWidgets() + { + int height = 0; + for (std::vector< std::pair >::iterator it = mLines.begin(); + it != mLines.end(); ++it) + { + height += (it->first)->getHeight(); + } + + bool scrollVisible = height > mScrollView->getHeight(); + int width = mScrollView->getWidth() - (scrollVisible ? 18 : 0); + + height = 0; + for (std::vector< std::pair >::iterator it = mLines.begin(); + it != mLines.end(); ++it) + { + int lineHeight = (it->first)->getHeight(); + (it->first)->setCoord(4, height, width-8, lineHeight); + if (it->second) + { + (it->second)->setCoord(4, height, width-8, lineHeight); + MyGUI::TextBox* second = (it->second)->castType(false); + if (second) + (it->first)->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.push_back(std::make_pair(separator, (MyGUI::Widget*)NULL)); + } + + 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.push_back(std::make_pair(groupWidget, groupWidget2)); + } + else + mLines.push_back(std::make_pair(groupWidget, (MyGUI::Widget*)NULL)); + } + + + void SpellView::setSize(const MyGUI::IntSize &_value) + { + bool changed = (_value.width != getWidth() || _value.height != getHeight()); + Base::setSize(_value); + if (changed) + layoutWidgets(); + } + + void SpellView::setSize(int _width, int _height) + { + setSize(MyGUI::IntSize(_width, _height)); + } + + void SpellView::setCoord(const MyGUI::IntCoord &_value) + { + bool changed = (_value.width != getWidth() || _value.height != getHeight()); + Base::setCoord(_value); + if (changed) + layoutWidgets(); + } + + void SpellView::setCoord(int _left, int _top, int _width, int _height) + { + setCoord(MyGUI::IntCoord(_left, _top, _width, _height)); + } + + void SpellView::adjustSpellWidget(const Spell &spell, SpellModel::ModelIndex index, MyGUI::Widget *widget) + { + if (spell.mType == Spell::Type_EnchantedItem) + { + widget->setUserData(spell.mItem); + widget->setUserString("ToolTipType", "ItemPtr"); + } + else + { + widget->setUserString("ToolTipType", "Spell"); + widget->setUserString("Spell", spell.mId); + } + + widget->setUserString("SpellModelIndex", MyGUI::utility::toString(index)); + + widget->eventMouseWheel += MyGUI::newDelegate(this, &SpellView::onMouseWheel); + widget->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellView::onSpellSelected); + } + + void SpellView::onSpellSelected(MyGUI::Widget* _sender) + { + SpellModel::ModelIndex i = MyGUI::utility::parseInt(_sender->getUserString("SpellModelIndex")); + eventSpellClicked(i); + } + + void SpellView::onMouseWheel(MyGUI::Widget* _sender, int _rel) + { + if (mScrollView->getViewOffset().top + _rel*0.3 > 0) + mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); + else + mScrollView->setViewOffset(MyGUI::IntPoint(0, mScrollView->getViewOffset().top + _rel*0.3)); + } + +} diff --git a/apps/openmw/mwgui/spellview.hpp b/apps/openmw/mwgui/spellview.hpp new file mode 100644 index 0000000000..30daf86711 --- /dev/null +++ b/apps/openmw/mwgui/spellview.hpp @@ -0,0 +1,71 @@ +#ifndef OPENMW_GUI_SPELLVIEW_H +#define OPENMW_GUI_SPELLVIEW_H + +#include + +#include "spellmodel.hpp" + +namespace MyGUI +{ + class ScrollView; +} + +namespace MWGui +{ + + class SpellModel; + + ///@brief Displays a SpellModel in a list widget + class SpellView : 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(); + + typedef MyGUI::delegates::CMultiDelegate1 EventHandle_ModelIndex; + /// Fired when a spell was clicked + EventHandle_ModelIndex eventSpellClicked; + + virtual void initialiseOverride(); + + virtual void setSize(const MyGUI::IntSize& _value); + virtual void setCoord(const MyGUI::IntCoord& _value); + void setSize(int _width, int _height); + void setCoord(int _left, int _top, int _width, int _height); + + private: + MyGUI::ScrollView* mScrollView; + + std::auto_ptr mModel; + + std::vector< std::pair > 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 onMouseWheel(MyGUI::Widget* _sender, int _rel); + }; + +} + +#endif diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index 7904c249ab..deb971e6d4 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -1,10 +1,7 @@ #include "spellwindow.hpp" -#include #include -#include - #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -19,44 +16,24 @@ #include "spellicons.hpp" #include "inventorywindow.hpp" #include "confirmationdialog.hpp" +#include "spellview.hpp" namespace MWGui { - bool sortItems(const MWWorld::Ptr& left, const MWWorld::Ptr& right) - { - int cmp = left.getClass().getName(left).compare( - right.getClass().getName(right)); - return cmp < 0; - } - - bool sortSpells(const std::string& left, const std::string& right) - { - const MWWorld::Store &spells = - MWBase::Environment::get().getWorld()->getStore().get(); - - const ESM::Spell* a = spells.find(left); - const ESM::Spell* b = spells.find(right); - - int cmp = a->mName.compare(b->mName); - return cmp < 0; - } - SpellWindow::SpellWindow(DragAndDrop* drag) : WindowPinnableBase("openmw_spell_window.layout") , NoDrop(drag, mMainWidget) - , mHeight(0) - , mWidth(0) - , mWindowSize(mMainWidget->getSize()) + , mSpellView(NULL) { mSpellIcons = new SpellIcons(); getWidget(mSpellView, "SpellView"); getWidget(mEffectBox, "EffectsBox"); - setCoord(498, 300, 302, 300); + mSpellView->eventSpellClicked += MyGUI::newDelegate(this, &SpellWindow::onModelIndexSelected); - mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &SpellWindow::onWindowResize); + setCoord(498, 300, 302, 300); } SpellWindow::~SpellWindow() @@ -84,261 +61,14 @@ namespace MWGui { mSpellIcons->updateWidgets(mEffectBox, false); - const int spellHeight = 18; - - mHeight = 0; - while (mSpellView->getChildCount()) - MyGUI::Gui::getInstance().destroyWidget(mSpellView->getChildAt(0)); - - // retrieve all player spells, divide them into Powers and Spells and sort them - std::vector spellList; - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); - MWMechanics::Spells& spells = stats.getSpells(); - - for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) - spellList.push_back (it->first); - - const MWWorld::ESMStore &esmStore = - MWBase::Environment::get().getWorld()->getStore(); - - std::vector powers; - std::vector::iterator it = spellList.begin(); - while (it != spellList.end()) - { - const ESM::Spell* spell = esmStore.get().find(*it); - - if (spell->mData.mType == ESM::Spell::ST_Power) - { - powers.push_back(*it); - it = spellList.erase(it); - } - else if (spell->mData.mType == ESM::Spell::ST_Ability - || spell->mData.mType == ESM::Spell::ST_Blight - || spell->mData.mType == ESM::Spell::ST_Curse - || spell->mData.mType == ESM::Spell::ST_Disease) - { - it = spellList.erase(it); - } - else - ++it; - } - std::sort(powers.begin(), powers.end(), sortSpells); - std::sort(spellList.begin(), spellList.end(), sortSpells); - - // retrieve player's enchanted items - std::vector items; - for (MWWorld::ContainerStoreIterator it(store.begin()); it != store.end(); ++it) - { - std::string enchantId = it->getClass().getEnchantment(*it); - if (enchantId != "") - { - // only add items with "Cast once" or "Cast on use" - const ESM::Enchantment* enchant = - esmStore.get().find(enchantId); - - int type = enchant->mData.mType; - if (type != ESM::Enchantment::CastOnce - && type != ESM::Enchantment::WhenUsed) - continue; - - items.push_back(*it); - } - } - std::sort(items.begin(), items.end(), sortItems); - - - int height = estimateHeight(items.size() + powers.size() + spellList.size()); - bool scrollVisible = height > mSpellView->getHeight(); - mWidth = mSpellView->getWidth() - (scrollVisible ? 18 : 0); - - // powers - addGroup("#{sPowers}", ""); - - for (std::vector::const_iterator it = powers.begin(); it != powers.end(); ++it) - { - const ESM::Spell* spell = esmStore.get().find(*it); - MyGUI::Button* t = mSpellView->createWidget("SandTextButton", - MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); - t->setCaption(spell->mName); - t->setTextAlign(MyGUI::Align::Left); - t->setUserString("ToolTipType", "Spell"); - t->setUserString("Spell", *it); - t->eventMouseWheel += MyGUI::newDelegate(this, &SpellWindow::onMouseWheel); - t->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellWindow::onSpellSelected); - - if (*it == MWBase::Environment::get().getWindowManager()->getSelectedSpell()) - t->setStateSelected(true); - - mHeight += spellHeight; - } - - // other spells - addGroup("#{sSpells}", "#{sCostChance}"); - for (std::vector::const_iterator it = spellList.begin(); it != spellList.end(); ++it) - { - const ESM::Spell* spell = esmStore.get().find(*it); - Gui::SharedStateButton* t = mSpellView->createWidget("SandTextButton", - MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); - t->setCaption(spell->mName); - t->setTextAlign(MyGUI::Align::Left); - t->setUserString("ToolTipType", "Spell"); - t->setUserString("Spell", *it); - t->eventMouseWheel += MyGUI::newDelegate(this, &SpellWindow::onMouseWheel); - t->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellWindow::onSpellSelected); - - // cost / success chance - Gui::SharedStateButton* costChance = mSpellView->createWidget("SandTextButton", - MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); - std::string cost = boost::lexical_cast(spell->mData.mCost); - std::string chance = boost::lexical_cast(int(MWMechanics::getSpellSuccessChance(*it, player))); - costChance->setCaption(cost + "/" + chance); - costChance->setTextAlign(MyGUI::Align::Right); - costChance->setUserString("ToolTipType", "Spell"); - costChance->setUserString("Spell", *it); - costChance->eventMouseWheel += MyGUI::newDelegate(this, &SpellWindow::onMouseWheel); - costChance->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellWindow::onSpellSelected); - - t->setSize(mWidth-12-costChance->getTextSize().width, t->getHeight()); - - Gui::ButtonGroup group; - group.push_back(t); - group.push_back(costChance); - Gui::SharedStateButton::createButtonGroup(group); - - t->setStateSelected(*it == MWBase::Environment::get().getWindowManager()->getSelectedSpell()); - - mHeight += spellHeight; - } - - - // enchanted items - addGroup("#{sMagicItem}", "#{sCostCharge}"); - - for (std::vector::const_iterator it = items.begin(); it != items.end(); ++it) - { - MWWorld::Ptr item = *it; - - const ESM::Enchantment* enchant = - esmStore.get().find(item.getClass().getEnchantment(item)); - - // check if the item is currently equipped (will display in a different color) - bool equipped = false; - for (int i=0; i < MWWorld::InventoryStore::Slots; ++i) - { - if (store.getSlot(i) != store.end() && *store.getSlot(i) == item) - { - equipped = true; - break; - } - } - - Gui::SharedStateButton* t = mSpellView->createWidget(equipped ? "SandTextButton" : "SpellTextUnequipped", - MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); - t->setCaption(item.getClass().getName(item)); - t->setTextAlign(MyGUI::Align::Left); - t->setUserData(item); - t->setUserString("ToolTipType", "ItemPtr"); - t->setUserString("Equipped", equipped ? "true" : "false"); - t->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellWindow::onEnchantedItemSelected); - t->eventMouseWheel += MyGUI::newDelegate(this, &SpellWindow::onMouseWheel); - - // cost / charge - Gui::SharedStateButton* costCharge = mSpellView->createWidget(equipped ? "SandTextButton" : "SpellTextUnequipped", - MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); - - float enchantCost = enchant->mData.mCost; - int eSkill = player.getClass().getSkill(player, ESM::Skill::Enchant); - int castCost = std::max(1.f, enchantCost - (enchantCost / 100) * (eSkill - 10)); - - std::string cost = boost::lexical_cast(castCost); - int currentCharge = int(item.getCellRef().getEnchantmentCharge()); - if (currentCharge == -1) - currentCharge = enchant->mData.mCharge; - std::string charge = boost::lexical_cast(currentCharge); - if (enchant->mData.mType == ESM::Enchantment::CastOnce) - { - // this is Morrowind behaviour - cost = "100"; - charge = "100"; - } - - - costCharge->setUserData(item); - costCharge->setUserString("ToolTipType", "ItemPtr"); - costCharge->setUserString("Equipped", equipped ? "true" : "false"); - costCharge->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellWindow::onEnchantedItemSelected); - costCharge->eventMouseWheel += MyGUI::newDelegate(this, &SpellWindow::onMouseWheel); - costCharge->setCaption(cost + "/" + charge); - costCharge->setTextAlign(MyGUI::Align::Right); - - Gui::ButtonGroup group; - group.push_back(t); - group.push_back(costCharge); - Gui::SharedStateButton::createButtonGroup(group); - - if (store.getSelectedEnchantItem() != store.end()) - t->setStateSelected(item == *store.getSelectedEnchantItem()); - - t->setSize(mWidth-12-costCharge->getTextSize().width, t->getHeight()); - - mHeight += spellHeight; - } - - // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden - mSpellView->setVisibleVScroll(false); - mSpellView->setCanvasSize(mSpellView->getWidth(), std::max(mSpellView->getHeight(), mHeight)); - mSpellView->setVisibleVScroll(true); + mSpellView->setModel(new SpellModel(MWBase::Environment::get().getWorld()->getPlayerPtr())); + mSpellView->update(); } - void SpellWindow::addGroup(const std::string &label, const std::string& label2) - { - if (mSpellView->getChildCount() > 0) - { - MyGUI::ImageBox* separator = mSpellView->createWidget("MW_HLine", - MyGUI::IntCoord(4, mHeight, mWidth-8, 18), - MyGUI::Align::Left | MyGUI::Align::Top); - separator->setNeedMouseFocus(false); - mHeight += 18; - } - - MyGUI::TextBox* groupWidget = mSpellView->createWidget("SandBrightText", - MyGUI::IntCoord(0, mHeight, mWidth, 24), - MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); - groupWidget->setCaptionWithReplacing(label); - groupWidget->setTextAlign(MyGUI::Align::Left); - groupWidget->setNeedMouseFocus(false); - - if (label2 != "") - { - MyGUI::TextBox* groupWidget2 = mSpellView->createWidget("SandBrightText", - MyGUI::IntCoord(0, mHeight, mWidth-4, 24), - MyGUI::Align::Left | MyGUI::Align::Top); - groupWidget2->setCaptionWithReplacing(label2); - groupWidget2->setTextAlign(MyGUI::Align::Right); - groupWidget2->setNeedMouseFocus(false); - - groupWidget->setSize(mWidth-8-groupWidget2->getTextSize().width, groupWidget->getHeight()); - } - - mHeight += 24; - } - - void SpellWindow::onWindowResize(MyGUI::Window* _sender) - { - if (mMainWidget->getSize() != mWindowSize) - { - mWindowSize = mMainWidget->getSize(); - updateSpells(); - } - } - - void SpellWindow::onEnchantedItemSelected(MyGUI::Widget* _sender) + void SpellWindow::onEnchantedItemSelected(MWWorld::Ptr item, bool alreadyEquipped) { MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - MWWorld::Ptr item = *_sender->getUserData(); // retrieve ContainerStoreIterator to the item MWWorld::ContainerStoreIterator it = store.begin(); @@ -352,7 +82,7 @@ namespace MWGui assert(it != store.end()); // equip, if it can be equipped and is not already equipped - if (_sender->getUserString("Equipped") == "false" + if (!alreadyEquipped && !item.getClass().getEquipmentSlots(item).first.empty()) { MWBase::Environment::get().getWindowManager()->getInventoryWindow()->useItem(item); @@ -364,12 +94,21 @@ namespace MWGui updateSpells(); } - void SpellWindow::onSpellSelected(MyGUI::Widget* _sender) + void SpellWindow::onModelIndexSelected(SpellModel::ModelIndex index) { - std::string spellId = _sender->getUserString("Spell"); - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); + const Spell& spell = mSpellView->getModel()->getItem(index); + if (spell.mType == Spell::Type_EnchantedItem) + { + onEnchantedItemSelected(spell.mItem, spell.mActive); + } + else + { + onSpellSelected(spell.mId); + } + } + void SpellWindow::onSpellSelected(const std::string& spellId) + { if (MyGUI::InputManager::getInstance().isShiftPressed()) { // delete spell, if allowed @@ -396,6 +135,8 @@ namespace MWGui } else { + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); store.setSelectedEnchantItem(store.end()); MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); } @@ -403,22 +144,6 @@ namespace MWGui updateSpells(); } - int SpellWindow::estimateHeight(int numSpells) const - { - int height = 0; - height += 24 * 3 + 18 * 2; // group headings - height += numSpells * 18; - return height; - } - - void SpellWindow::onMouseWheel(MyGUI::Widget* _sender, int _rel) - { - if (mSpellView->getViewOffset().top + _rel*0.3 > 0) - mSpellView->setViewOffset(MyGUI::IntPoint(0, 0)); - else - mSpellView->setViewOffset(MyGUI::IntPoint(0, mSpellView->getViewOffset().top + _rel*0.3)); - } - void SpellWindow::onDeleteSpellAccept() { MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); diff --git a/apps/openmw/mwgui/spellwindow.hpp b/apps/openmw/mwgui/spellwindow.hpp index a74847b907..2f7319cb92 100644 --- a/apps/openmw/mwgui/spellwindow.hpp +++ b/apps/openmw/mwgui/spellwindow.hpp @@ -4,13 +4,12 @@ #include "windowpinnablebase.hpp" #include "../mwworld/ptr.hpp" +#include "spellmodel.hpp" + namespace MWGui { class SpellIcons; - - bool sortItems(const MWWorld::Ptr& left, const MWWorld::Ptr& right); - - bool sortSpells(const std::string& left, const std::string& right); + class SpellView; class SpellWindow : public WindowPinnableBase, public NoDrop { @@ -23,30 +22,20 @@ namespace MWGui void onFrame(float dt) { NoDrop::onFrame(dt); } protected: - MyGUI::ScrollView* mSpellView; MyGUI::Widget* mEffectBox; - int mHeight; - int mWidth; - - MyGUI::IntSize mWindowSize; - std::string mSpellToDelete; - void addGroup(const std::string& label, const std::string& label2); - - int estimateHeight(int numSpells) const; - - void onWindowResize(MyGUI::Window* _sender); - void onEnchantedItemSelected(MyGUI::Widget* _sender); - void onSpellSelected(MyGUI::Widget* _sender); - void onMouseWheel(MyGUI::Widget* _sender, int _rel); + void onEnchantedItemSelected(MWWorld::Ptr item, bool alreadyEquipped); + void onSpellSelected(const std::string& spellId); + void onModelIndexSelected(SpellModel::ModelIndex index); void onDeleteSpellAccept(); virtual void onPinToggled(); virtual void onTitleDoubleClicked(); virtual void open(); + SpellView* mSpellView; SpellIcons* mSpellIcons; }; } diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index acb8b2eb75..26f14572f2 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -73,6 +73,7 @@ #include "itemwidget.hpp" #include "screenfader.hpp" #include "debugwindow.hpp" +#include "spellview.hpp" namespace MWGui { @@ -179,6 +180,7 @@ namespace MWGui BookPage::registerMyGUIComponents (); ItemView::registerComponents(); ItemWidget::registerComponents(); + SpellView::registerComponents(); Gui::registerAllWidgets(); MyGUI::FactoryManager::getInstance().registerFactory("Controller"); diff --git a/files/mygui/openmw_list.skin.xml b/files/mygui/openmw_list.skin.xml index 21b17c8f07..ce8209e3d5 100644 --- a/files/mygui/openmw_list.skin.xml +++ b/files/mygui/openmw_list.skin.xml @@ -157,6 +157,15 @@ + + + + + + + + + diff --git a/files/mygui/openmw_magicselection_dialog.layout b/files/mygui/openmw_magicselection_dialog.layout index a89795473f..bf4cb71d48 100644 --- a/files/mygui/openmw_magicselection_dialog.layout +++ b/files/mygui/openmw_magicselection_dialog.layout @@ -3,12 +3,10 @@ - + - - - + diff --git a/files/mygui/openmw_spell_window.layout b/files/mygui/openmw_spell_window.layout index ec655ace81..21bf74267a 100644 --- a/files/mygui/openmw_spell_window.layout +++ b/files/mygui/openmw_spell_window.layout @@ -10,10 +10,7 @@ - - - - + From 4e0d16da8c4651d006f3b40e05e50e5eea82243e Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 15 Dec 2014 13:34:04 +0100 Subject: [PATCH 267/303] Take Scale field in creature record into account (Fixes #2214) --- apps/openmw/mwclass/creature.cpp | 6 ++++++ apps/openmw/mwclass/creature.hpp | 2 ++ apps/openmw/mwworld/physicssystem.cpp | 7 +++++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 519216ec9b..9d07aecf3f 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -897,4 +897,10 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mAiData.mFight; } + + void Creature::adjustScale(const MWWorld::Ptr &ptr, float &scale) const + { + MWWorld::LiveCellRef *ref = ptr.get(); + scale *= ref->mBase->mScale; + } } diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp index c52e85534b..4b58864489 100644 --- a/apps/openmw/mwclass/creature.hpp +++ b/apps/openmw/mwclass/creature.hpp @@ -156,6 +156,8 @@ namespace MWClass virtual void restock (const MWWorld::Ptr &ptr) const; virtual int getBaseFightRating(const MWWorld::Ptr &ptr) const; + + virtual void adjustScale(const MWWorld::Ptr& ptr,float& scale) const; }; } diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index eecc4a02db..61a3b27f81 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -754,8 +754,11 @@ namespace MWWorld if (OEngine::Physic::PhysicActor* act = mEngine->getCharacter(handle)) { - // NOTE: Ignoring Npc::adjustScale (race height) on purpose. This is a bug in MW and must be replicated for compatibility reasons - act->setScale(ptr.getCellRef().getScale()); + float scale = ptr.getCellRef().getScale(); + if (!ptr.getClass().isNpc()) + // NOTE: Ignoring Npc::adjustScale (race height) on purpose. This is a bug in MW and must be replicated for compatibility reasons + ptr.getClass().adjustScale(ptr, scale); + act->setScale(scale); } } From 4d5adfb5ddec13c21ebead5d405b512a9dc379e9 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 15 Dec 2014 13:47:34 +0100 Subject: [PATCH 268/303] Fix being able to use enchantments of items that failed to equip (Fixes #2215) --- apps/openmw/mwgui/inventoryitemmodel.cpp | 10 ++-------- apps/openmw/mwgui/quickkeysmenu.cpp | 4 ++++ apps/openmw/mwgui/spellmodel.cpp | 11 +---------- apps/openmw/mwgui/spellwindow.cpp | 3 +++ apps/openmw/mwgui/tradeitemmodel.cpp | 11 +---------- apps/openmw/mwmechanics/aicombataction.cpp | 4 ++++ apps/openmw/mwworld/inventorystore.cpp | 10 ++++++++++ apps/openmw/mwworld/inventorystore.hpp | 3 +++ 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/apps/openmw/mwgui/inventoryitemmodel.cpp b/apps/openmw/mwgui/inventoryitemmodel.cpp index f45881770a..cee0dc6eed 100644 --- a/apps/openmw/mwgui/inventoryitemmodel.cpp +++ b/apps/openmw/mwgui/inventoryitemmodel.cpp @@ -99,14 +99,8 @@ void InventoryItemModel::update() if (mActor.getClass().hasInventoryStore(mActor)) { MWWorld::InventoryStore& store = mActor.getClass().getInventoryStore(mActor); - for (int slot=0; slotgetInventoryWindow()->useItem(item); + + // make sure that item was successfully equipped + if (!store.isEquipped(item)) + return; } store.setSelectedEnchantItem(it); diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp index 0305258978..fe2f073712 100644 --- a/apps/openmw/mwgui/spellmodel.cpp +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -108,16 +108,7 @@ namespace MWGui std::string charge = boost::lexical_cast(currentCharge); newSpell.mCostColumn = cost + "/" + charge; - bool equipped = false; - for (int i=0; i < MWWorld::InventoryStore::Slots; ++i) - { - if (invStore.getSlot(i) != invStore.end() && *invStore.getSlot(i) == item) - { - equipped = true; - break; - } - } - newSpell.mActive = equipped; + newSpell.mActive = invStore.isEquipped(item); } mSpells.push_back(newSpell); } diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index deb971e6d4..1da9635db3 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -86,6 +86,9 @@ namespace MWGui && !item.getClass().getEquipmentSlots(item).first.empty()) { MWBase::Environment::get().getWindowManager()->getInventoryWindow()->useItem(item); + // make sure that item was successfully equipped + if (!store.isEquipped(item)) + return; } MWBase::Environment::get().getWindowManager()->unsetSelectedSpell(); diff --git a/apps/openmw/mwgui/tradeitemmodel.cpp b/apps/openmw/mwgui/tradeitemmodel.cpp index 3abfac997b..df0cbcac95 100644 --- a/apps/openmw/mwgui/tradeitemmodel.cpp +++ b/apps/openmw/mwgui/tradeitemmodel.cpp @@ -175,17 +175,8 @@ namespace MWGui // don't show equipped items if(mMerchant.getClass().hasInventoryStore(mMerchant)) { - bool isEquipped = false; MWWorld::InventoryStore& store = mMerchant.getClass().getInventoryStore(mMerchant); - for (int slot=0; slotgetStore().get().find(ptr.getClass().getEnchantment(ptr)); + if (enchantment->mData.mType == ESM::Enchantment::CastOnce) { return rateEffects(enchantment->mEffects, actor, target); } else + { + //if (!ptr.getClass().canBeEquipped(ptr, actor)) return 0.f; + } } float rateEffect(const ESM::ENAMstruct &effect, const MWWorld::Ptr &actor, const MWWorld::Ptr &target) diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index c577d4b0d2..fef34d67be 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -643,3 +643,13 @@ void MWWorld::InventoryStore::clear() initSlots (mSlots); ContainerStore::clear(); } + +bool MWWorld::InventoryStore::isEquipped(const MWWorld::Ptr &item) +{ + for (int i=0; i < MWWorld::InventoryStore::Slots; ++i) + { + if (getSlot(i) != end() && *getSlot(i) == item) + return true; + } + return false; +} diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index 16d965cda3..30abc2ea57 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -145,6 +145,9 @@ namespace MWWorld 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::Ptr& 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 From 935cccf9745dc04c37a85321bed9adef557f9831 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 15 Dec 2014 15:23:03 +0100 Subject: [PATCH 269/303] Implement weapon/spell cycling hotkeys (Fixes #1024) --- apps/openmw/mwbase/windowmanager.hpp | 5 +++ apps/openmw/mwgui/inventorywindow.cpp | 48 +++++++++++++++++++++++ apps/openmw/mwgui/inventorywindow.hpp | 3 ++ apps/openmw/mwgui/sortfilteritemmodel.cpp | 32 +++++++++------ apps/openmw/mwgui/sortfilteritemmodel.hpp | 4 ++ apps/openmw/mwgui/spellmodel.cpp | 4 ++ apps/openmw/mwgui/spellwindow.cpp | 21 ++++++++++ apps/openmw/mwgui/spellwindow.hpp | 3 ++ apps/openmw/mwgui/windowmanagerimp.cpp | 10 +++++ apps/openmw/mwgui/windowmanagerimp.hpp | 5 +++ apps/openmw/mwinput/inputmanagerimp.cpp | 25 ++++++++++++ 11 files changed, 148 insertions(+), 12 deletions(-) diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index d4f1afa32d..fb35157e85 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -342,6 +342,11 @@ namespace MWBase virtual void setWerewolfOverlay(bool set) = 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; }; } diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index d1eeba1573..4fbfc2c67a 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -27,6 +27,19 @@ #include "tradewindow.hpp" #include "container.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 { @@ -599,4 +612,39 @@ namespace MWGui MWBase::Environment::get().getMechanicsManager()->itemTaken(player, newObject, count); } + + void InventoryWindow::cycle(bool next) + { + ItemModel::ModelIndex selected = 0; + // not using mSortFilterModel as we only need sorting, not filtering + SortFilterItemModel model(new InventoryItemModel(MWBase::Environment::get().getWorld()->getPlayerPtr())); + model.setSortByType(false); + model.update(); + if (model.getItemCount() == 0) + return; + for (ItemModel::ModelIndex i=0; imData.mCost; int eSkill = mActor.getClass().getSkill(mActor, ESM::Skill::Enchant); int castCost = std::max(1.f, enchantCost - (enchantCost / 100) * (eSkill - 10)); diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index 1da9635db3..37747f957d 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -160,4 +160,25 @@ namespace MWGui updateSpells(); } + + void SpellWindow::cycle(bool next) + { + mSpellView->setModel(new SpellModel(MWBase::Environment::get().getWorld()->getPlayerPtr())); + mSpellView->getModel()->update(); + + SpellModel::ModelIndex selected = 0; + for (SpellModel::ModelIndex i = 0; igetModel()->getItemCount()); ++i) + { + if (mSpellView->getModel()->getItem(i).mSelected) + selected = i; + } + + selected += next ? 1 : -1; + int itemcount = mSpellView->getModel()->getItemCount(); + if (itemcount == 0) + return; + selected = (selected + itemcount) % itemcount; + + onModelIndexSelected(selected); + } } diff --git a/apps/openmw/mwgui/spellwindow.hpp b/apps/openmw/mwgui/spellwindow.hpp index 2f7319cb92..650218d307 100644 --- a/apps/openmw/mwgui/spellwindow.hpp +++ b/apps/openmw/mwgui/spellwindow.hpp @@ -21,6 +21,9 @@ namespace MWGui void onFrame(float dt) { NoDrop::onFrame(dt); } + /// Cycle to next/previous spell + void cycle(bool next); + protected: MyGUI::Widget* mEffectBox; diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 26f14572f2..9d24fb19bd 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -1820,4 +1820,14 @@ namespace MWGui mDebugWindow->setVisible(!mDebugWindow->isVisible()); } + void WindowManager::cycleSpell(bool next) + { + mSpellWindow->cycle(next); + } + + void WindowManager::cycleWeapon(bool next) + { + mInventoryWindow->cycle(next); + } + } diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 94d8a93db2..015f200d58 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -338,6 +338,11 @@ namespace MWGui virtual void toggleDebugWindow(); + /// Cycle to next or previous spell + virtual void cycleSpell(bool next); + /// Cycle to next or previous weapon + virtual void cycleWeapon(bool next); + private: bool mConsoleOnlyScripts; diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index b0d2bfefff..2f6149f091 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -286,6 +286,18 @@ namespace MWInput case A_QuickLoad: quickLoad(); break; + case A_CycleSpellLeft: + MWBase::Environment::get().getWindowManager()->cycleSpell(false); + break; + case A_CycleSpellRight: + MWBase::Environment::get().getWindowManager()->cycleSpell(true); + break; + case A_CycleWeaponLeft: + MWBase::Environment::get().getWindowManager()->cycleWeapon(false); + break; + case A_CycleWeaponRight: + MWBase::Environment::get().getWindowManager()->cycleWeapon(true); + break; } } } @@ -894,6 +906,11 @@ namespace MWInput 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; @@ -972,6 +989,10 @@ namespace MWInput descriptions[A_MoveRight] = "sRight"; descriptions[A_ToggleWeapon] = "sReady_Weapon"; descriptions[A_ToggleSpell] = "sReady_Magic"; + descriptions[A_CycleSpellLeft] = "sPrevSpell"; + descriptions[A_CycleSpellRight] = "sNextSpell"; + descriptions[A_CycleWeaponLeft] = "sPrevWeapon"; + descriptions[A_CycleWeaponRight] = "sNextWeapon"; descriptions[A_Console] = "sConsoleTitle"; descriptions[A_Run] = "sRun"; descriptions[A_Sneak] = "sCrouch_Sneak"; @@ -1032,6 +1053,10 @@ namespace MWInput ret.push_back(A_Use); ret.push_back(A_ToggleWeapon); ret.push_back(A_ToggleSpell); + ret.push_back(A_CycleSpellLeft); + ret.push_back(A_CycleSpellRight); + ret.push_back(A_CycleWeaponLeft); + ret.push_back(A_CycleWeaponRight); ret.push_back(A_AutoMove); ret.push_back(A_Jump); ret.push_back(A_Inventory); From c7e1c0b595dd88c4fc61134a00bb3266ba156455 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 15 Dec 2014 17:38:32 +0100 Subject: [PATCH 270/303] Fix weapon cycle getting stuck for same item IDs --- apps/openmw/mwgui/inventorywindow.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index 4fbfc2c67a..d3c6073f47 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -615,13 +615,14 @@ namespace MWGui void InventoryWindow::cycle(bool next) { - ItemModel::ModelIndex selected = 0; + ItemModel::ModelIndex selected = -1; // not using mSortFilterModel as we only need sorting, not filtering SortFilterItemModel model(new InventoryItemModel(MWBase::Environment::get().getWorld()->getPlayerPtr())); model.setSortByType(false); model.update(); if (model.getItemCount() == 0) return; + for (ItemModel::ModelIndex i=0; i Date: Mon, 15 Dec 2014 18:19:05 +0100 Subject: [PATCH 271/303] Work around particles not being rendered in the first frame --- components/nifogre/ogrenifloader.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/nifogre/ogrenifloader.cpp b/components/nifogre/ogrenifloader.cpp index cdbc4d8447..22685f5489 100644 --- a/components/nifogre/ogrenifloader.cpp +++ b/components/nifogre/ogrenifloader.cpp @@ -1033,7 +1033,7 @@ class NIFObjectLoader static void createParticleInitialState(Ogre::ParticleSystem* partsys, const Nif::NiAutoNormalParticlesData* particledata, const Nif::NiParticleSystemController* partctrl) { - partsys->_update(0.f); // seems to be required to allocate mFreeParticles + partsys->_update(0.f); // seems to be required to allocate mFreeParticles. TODO: patch Ogre to handle this better int i=0; for (std::vector::const_iterator it = partctrl->particles.begin(); iactiveCount && it != partctrl->particles.end(); ++it, ++i) @@ -1073,6 +1073,7 @@ class NIFObjectLoader totalTimeToLive = std::max(0.f, particle.lifespan); timeToLive = std::max(0.f, particle.lifespan - particle.lifetime); } + partsys->_update(0.f); // now apparently needs another update, otherwise it won't render in the first frame. TODO: patch Ogre to handle this better } static void createNodeControllers(const Nif::NIFFilePtr& nif, const std::string &name, Nif::ControllerPtr ctrl, ObjectScenePtr scene, int animflags) From e4127aa491fa39eac68b43fc5fd4c4301d085200 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 15 Dec 2014 19:04:53 +0100 Subject: [PATCH 272/303] Use space in ItemView more efficiently --- apps/openmw/mwgui/itemview.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwgui/itemview.cpp b/apps/openmw/mwgui/itemview.cpp index a51ada2754..6af6a3b3f6 100644 --- a/apps/openmw/mwgui/itemview.cpp +++ b/apps/openmw/mwgui/itemview.cpp @@ -53,9 +53,14 @@ void ItemView::layoutWidgets() int x = 0; int y = 0; - int maxHeight = mScrollView->getSize().height - 58; - MyGUI::Widget* dragArea = mScrollView->getChildAt(0); + int maxHeight = dragArea->getHeight(); + + int rows = maxHeight/42; + rows = std::max(rows, 1); + bool showScrollbar = std::ceil(dragArea->getChildCount()/float(rows)) > mScrollView->getWidth()/42; + if (showScrollbar) + maxHeight -= 18; for (unsigned int i=0; igetChildCount(); ++i) { @@ -64,7 +69,8 @@ void ItemView::layoutWidgets() w->setPosition(x, y); y += 42; - if (y > maxHeight) + + if (y > maxHeight-42 && i < dragArea->getChildCount()-1) { x += 42; y = 0; From ec00c830e5a1ae8afc195a8149ae4ba0e62fb6c2 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 15 Dec 2014 19:18:14 +0100 Subject: [PATCH 273/303] Fix missing armor rating label update --- apps/openmw/mwgui/inventorywindow.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index d3c6073f47..c1a5ab94a5 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -527,6 +527,14 @@ namespace MWGui void InventoryWindow::doRenderUpdate () { mPreview->onFrame(); + + if (mPreviewResize || mPreviewDirty) + { + mArmorRating->setCaptionWithReplacing ("#{sArmor}: " + + boost::lexical_cast(static_cast(mPtr.getClass().getArmorRating(mPtr)))); + if (mArmorRating->getTextSize().width > mArmorRating->getSize().width) + mArmorRating->setCaptionWithReplacing (boost::lexical_cast(static_cast(mPtr.getClass().getArmorRating(mPtr)))); + } if (mPreviewResize) { mPreviewResize = false; @@ -543,11 +551,6 @@ namespace MWGui mPreview->update (); mAvatarImage->setImageTexture("CharacterPreview"); - - mArmorRating->setCaptionWithReplacing ("#{sArmor}: " - + boost::lexical_cast(static_cast(mPtr.getClass().getArmorRating(mPtr)))); - if (mArmorRating->getTextSize().width > mArmorRating->getSize().width) - mArmorRating->setCaptionWithReplacing (boost::lexical_cast(static_cast(mPtr.getClass().getArmorRating(mPtr)))); } } From 0dc94012698a2713fc89ae2526a1652d6a51d633 Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 15 Dec 2014 20:20:17 +0100 Subject: [PATCH 274/303] Fix GUI crash due to outdated spells list --- apps/openmw/mwgui/container.cpp | 2 ++ apps/openmw/mwgui/inventorywindow.cpp | 6 ++++++ apps/openmw/mwgui/spellwindow.cpp | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwgui/container.cpp b/apps/openmw/mwgui/container.cpp index 58f23c175b..6df8a3f448 100644 --- a/apps/openmw/mwgui/container.cpp +++ b/apps/openmw/mwgui/container.cpp @@ -124,6 +124,8 @@ namespace MWGui 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(); } diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index c1a5ab94a5..60f40e6fb3 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -309,6 +309,9 @@ namespace MWGui void InventoryWindow::updateItemView() { + if (MWBase::Environment::get().getWindowManager()->getSpellWindow()) + MWBase::Environment::get().getWindowManager()->getSpellWindow()->updateSpells(); + mItemView->update(); mPreviewDirty = true; } @@ -614,6 +617,9 @@ namespace MWGui mDragAndDrop->startDrag(i, mSortModel, mTradeModel, mItemView, count); MWBase::Environment::get().getMechanicsManager()->itemTaken(player, newObject, count); + + if (MWBase::Environment::get().getWindowManager()->getSpellWindow()) + MWBase::Environment::get().getWindowManager()->getSpellWindow()->updateSpells(); } void InventoryWindow::cycle(bool next) diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index 37747f957d..240d0419e3 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -79,7 +79,8 @@ namespace MWGui break; } } - assert(it != store.end()); + 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 From 830ecfc70b9bfa7804addf94eb5211bee91e06c5 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 16 Dec 2014 11:27:13 +0100 Subject: [PATCH 275/303] updated changelog once more --- readme.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.txt b/readme.txt index f5e3e0e59b..5d344c0b50 100644 --- a/readme.txt +++ b/readme.txt @@ -163,6 +163,7 @@ Bug #2163: OpenMW can't detect death if the NPC die by the post damage effect of 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 From b9e5aa9db677a4ab0dd3913c6213c1a5cad39d97 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 16 Dec 2014 20:44:42 +0100 Subject: [PATCH 276/303] Movement controller: Don't allow stepping up other actors This seems to fix issues with NPCs inadvertently being placed on top of a small creature while fighting it. Note that jumping on top of actors is still possible (Bug #1192) --- apps/openmw/mwworld/physicssystem.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 61a3b27f81..aeaa07a925 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -195,6 +195,9 @@ namespace MWWorld stepper.doTrace(colobj, tracer.mEndPos, tracer.mEndPos-Ogre::Vector3(0.0f,0.0f,sStepSize), engine); if(stepper.mFraction < 1.0f && getSlope(stepper.mPlaneNormal) <= sMaxSlope) { + // don't allow stepping up other actors + if (stepper.mHitObject->getBroadphaseHandle()->m_collisionFilterGroup == OEngine::Physic::CollisionType_Actor) + return false; // only step down onto semi-horizontal surfaces. don't step down onto the side of a house or a wall. // TODO: stepper.mPlaneNormal does not appear to be reliable - needs more testing // NOTE: caller's variables 'position' & 'remainingTime' are modified here From d962f0918dee426a7dce21cf7719de0c6fe0afae Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 16 Dec 2014 20:47:45 +0100 Subject: [PATCH 277/303] Implement NPC head tracking (Fixes #1720) --- apps/openmw/mwmechanics/actors.cpp | 51 ++++++++++++++++++++++ apps/openmw/mwmechanics/actors.hpp | 3 ++ apps/openmw/mwmechanics/character.cpp | 62 +++++++++++++++++++++++++++ apps/openmw/mwmechanics/character.hpp | 7 +++ apps/openmw/mwrender/animation.hpp | 4 ++ apps/openmw/mwrender/npcanimation.cpp | 31 +++++++++++++- apps/openmw/mwrender/npcanimation.hpp | 8 ++++ 7 files changed, 165 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index fecf189861..3a9ba56184 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -4,6 +4,7 @@ #include #include +#include #include @@ -273,6 +274,40 @@ namespace MWMechanics calculateRestoration(ptr, duration); } + void Actors::updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, + MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance) + { + static const float fMaxHeadTrackDistance = MWBase::Environment::get().getWorld()->getStore().get() + .find("fMaxHeadTrackDistance")->getFloat(); + static const float fInteriorHeadTrackMult = MWBase::Environment::get().getWorld()->getStore().get() + .find("fInteriorHeadTrackMult")->getFloat(); + float maxDistance = fMaxHeadTrackDistance; + const ESM::Cell* currentCell = actor.getCell()->getCell(); + if (!currentCell->isExterior() && !(currentCell->mData.mFlags & ESM::Cell::QuasiEx)) + maxDistance *= fInteriorHeadTrackMult; + + const ESM::Position& actor1Pos = actor.getRefData().getPosition(); + const ESM::Position& actor2Pos = targetActor.getRefData().getPosition(); + float sqrDist = Ogre::Vector3(actor1Pos.pos).squaredDistance(Ogre::Vector3(actor2Pos.pos)); + + if (sqrDist > maxDistance*maxDistance) + return; + + // stop tracking when target is behind the actor + Ogre::Vector3 actorDirection (actor.getRefData().getBaseNode()->getOrientation().yAxis()); + Ogre::Vector3 targetDirection (Ogre::Vector3(actor2Pos.pos) - Ogre::Vector3(actor1Pos.pos)); + actorDirection.z = 0; + targetDirection.z = 0; + if (actorDirection.angleBetween(targetDirection) < Ogre::Degree(90) + && sqrDist <= sqrHeadTrackDistance + && 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::engageCombat (const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, bool againstPlayer) { CreatureStats& creatureStats = actor1.getClass().getCreatureStats(actor1); @@ -1138,9 +1173,11 @@ namespace MWMechanics if(!paused) { static float timerUpdateAITargets = 0; + static float timerUpdateHeadTrack = 0; // target lists get updated once every 1.0 sec if (timerUpdateAITargets >= 1.0f) timerUpdateAITargets = 0; + if (timerUpdateHeadTrack >= 0.3f) timerUpdateHeadTrack = 0; MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); @@ -1174,6 +1211,19 @@ namespace MWMechanics engageCombat(iter->first, it->first, it->first == player); } } + if (timerUpdateHeadTrack == 0) + { + float sqrHeadTrackDistance = std::numeric_limits::max(); + MWWorld::Ptr headTrackTarget; + + for(PtrControllerMap::iterator it(mActors.begin()); it != mActors.end(); ++it) + { + if (it->first == iter->first) + continue; + updateHeadTracking(iter->first, it->first, headTrackTarget, sqrHeadTrackDistance); + } + iter->second->setHeadTrackTarget(headTrackTarget); + } if (iter->first.getClass().isNpc() && iter->first != player) updateCrimePersuit(iter->first, duration); @@ -1194,6 +1244,7 @@ namespace MWMechanics } timerUpdateAITargets += duration; + timerUpdateHeadTrack += duration; // Looping magic VFX update // Note: we need to do this before any of the animations are updated. diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index a30a9dcf01..321229571f 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -89,6 +89,9 @@ namespace MWMechanics */ void engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, bool againstPlayer); + void updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, + MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance); + void restoreDynamicStats(bool sleep); ///< If the player is sleeping, this should be called every hour. diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index d8693fbcdf..d675b0157e 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -42,6 +42,15 @@ namespace { +// Wraps a value to (-PI, PI] +void wrap(Ogre::Radian& rad) +{ + if (rad.valueRadians()>0) + rad = Ogre::Radian(std::fmod(rad.valueRadians()+Ogre::Math::PI, 2.0f*Ogre::Math::PI)-Ogre::Math::PI); + else + rad = Ogre::Radian(std::fmod(rad.valueRadians()-Ogre::Math::PI, 2.0f*Ogre::Math::PI)+Ogre::Math::PI); +} + std::string getBestAttack (const ESM::Weapon* weapon) { int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2; @@ -1627,6 +1636,8 @@ void CharacterController::update(float duration) cls.getMovementSettings(mPtr).mPosition[0] = cls.getMovementSettings(mPtr).mPosition[1] = 0; // Can't reset jump state (mPosition[2]) here; 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. + + updateHeadTracking(duration); } else if(cls.getCreatureStats(mPtr).isDead()) { @@ -1845,4 +1856,55 @@ bool CharacterController::isKnockedOut() const return mHitState == CharState_KnockOut; } +void CharacterController::setHeadTrackTarget(const MWWorld::Ptr &target) +{ + mHeadTrackTarget = target; +} + +void CharacterController::updateHeadTracking(float duration) +{ + Ogre::Node* head = mAnimation->getNode("Bip01 Head"); + if (!head) + return; + Ogre::Radian zAngle (0.f); + Ogre::Radian xAngle (0.f); + if (!mHeadTrackTarget.isEmpty()) + { + Ogre::Vector3 headPos = mPtr.getRefData().getBaseNode()->convertLocalToWorldPosition(head->_getDerivedPosition()); + Ogre::Vector3 targetPos (mHeadTrackTarget.getRefData().getPosition().pos); + if (MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mHeadTrackTarget)) + { + Ogre::Node* targetHead = anim->getNode("Head"); + if (!targetHead) + targetHead = anim->getNode("Bip01 Head"); + if (targetHead) + targetPos = mHeadTrackTarget.getRefData().getBaseNode()->convertLocalToWorldPosition( + targetHead->_getDerivedPosition()); + } + + Ogre::Vector3 direction = targetPos - headPos; + direction.normalise(); + + const Ogre::Vector3 actorDirection = mPtr.getRefData().getBaseNode()->getOrientation().yAxis(); + + zAngle = Ogre::Math::ATan2(direction.x,direction.y) - + Ogre::Math::ATan2(actorDirection.x, actorDirection.y); + xAngle = -Ogre::Math::ASin(direction.z); + wrap(zAngle); + wrap(xAngle); + xAngle = Ogre::Degree(std::min(xAngle.valueDegrees(), 40.f)); + xAngle = Ogre::Degree(std::max(xAngle.valueDegrees(), -40.f)); + zAngle = Ogre::Degree(std::min(zAngle.valueDegrees(), 30.f)); + zAngle = Ogre::Degree(std::max(zAngle.valueDegrees(), -30.f)); + + } + float factor = duration*5; + factor = std::min(factor, 1.f); + xAngle = (1.f-factor) * mAnimation->getHeadPitch() + factor * (-xAngle); + zAngle = (1.f-factor) * mAnimation->getHeadYaw() + factor * (-zAngle); + + mAnimation->setHeadPitch(xAngle); + mAnimation->setHeadYaw(zAngle); +} + } diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index d7028a8441..5b2c57b0a9 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -175,6 +175,8 @@ class CharacterController float mSecondsOfSwimming; float mSecondsOfRunning; + MWWorld::Ptr mHeadTrackTarget; + float mTurnAnimationThreshold; // how long to continue playing turning animation after actor stopped turning std::string mAttackType; // slash, chop or thrust @@ -188,6 +190,8 @@ class CharacterController bool updateCreatureState(); void updateIdleStormState(); + void updateHeadTracking(float duration); + void castSpell(const std::string& spellid); void updateMagicEffects(); @@ -229,6 +233,9 @@ public: bool isReadyToBlock() const; bool isKnockedOut() const; + + /// Make this character turn its head towards \a target. To turn off head tracking, pass an empty Ptr. + void setHeadTrackTarget(const MWWorld::Ptr& target); }; void getWeaponGroup(WeaponType weaptype, std::string &group); diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index d25d4f0b0e..1a420582cd 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -306,6 +306,10 @@ public: /// A relative factor (0-1) that decides if and how much the skeleton should be pitched /// to indicate the facing orientation of the character. virtual void setPitchFactor(float factor) {} + virtual void setHeadPitch(Ogre::Radian factor) {} + virtual void setHeadYaw(Ogre::Radian factor) {} + virtual Ogre::Radian getHeadPitch() const { return Ogre::Radian(0.f); } + virtual Ogre::Radian getHeadYaw() const { return Ogre::Radian(0.f); } virtual Ogre::Vector3 runAnimation(float duration); diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index b93b37aeac..7c68a45380 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -207,7 +207,9 @@ NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, Ogre::SceneNode* node, int v mFirstPersonOffset(0.f, 0.f, 0.f), mAlpha(1.f), mNpcType(Type_Normal), - mSoundsDisabled(disableSounds) + mSoundsDisabled(disableSounds), + mHeadPitch(0.f), + mHeadYaw(0.f) { mNpc = mPtr.get()->mBase; @@ -621,6 +623,13 @@ Ogre::Vector3 NpcAnimation::runAnimation(float timepassed) { // In third person mode we may still need pitch for ranged weapon targeting pitchSkeleton(mPtr.getRefData().getPosition().rot[0], baseinst); + + Ogre::Node* node = baseinst->getBone("Bip01 Head"); + if (node) + { + node->rotate(Ogre::Vector3::UNIT_Z, mHeadYaw, Ogre::Node::TS_WORLD); + node->rotate(Ogre::Vector3::UNIT_X, mHeadPitch, Ogre::Node::TS_WORLD); + } } mFirstPersonOffset = 0.f; // reset the X, Y, Z offset for the next frame. @@ -993,4 +1002,24 @@ void NpcAnimation::setVampire(bool vampire) } } +void NpcAnimation::setHeadPitch(Ogre::Radian pitch) +{ + mHeadPitch = pitch; +} + +void NpcAnimation::setHeadYaw(Ogre::Radian yaw) +{ + mHeadYaw = yaw; +} + +Ogre::Radian NpcAnimation::getHeadPitch() const +{ + return mHeadPitch; +} + +Ogre::Radian NpcAnimation::getHeadYaw() const +{ + return mHeadYaw; +} + } diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index aba01bcfaa..f3603fe14b 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -100,6 +100,9 @@ private: float mAlpha; bool mSoundsDisabled; + Ogre::Radian mHeadYaw; + Ogre::Radian mHeadPitch; + void updateNpcBase(); NifOgre::ObjectScenePtr insertBoundedPart(const std::string &model, int group, const std::string &bonename, @@ -142,6 +145,11 @@ public: /// to indicate the facing orientation of the character. virtual void setPitchFactor(float factor) { mPitchFactor = factor; } + virtual void setHeadPitch(Ogre::Radian pitch); + virtual void setHeadYaw(Ogre::Radian yaw); + virtual Ogre::Radian getHeadPitch() const; + virtual Ogre::Radian getHeadYaw() const; + virtual void showWeapons(bool showWeapon); virtual void showCarriedLeft(bool show); From 88c5e1991c13fe3f877335256139fc2040613352 Mon Sep 17 00:00:00 2001 From: scrawl Date: Tue, 16 Dec 2014 23:18:41 +0100 Subject: [PATCH 278/303] Fix being able to stand on top of actors (Fixes #1192) --- apps/openmw/mwworld/physicssystem.cpp | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index aeaa07a925..d9941bafdc 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -452,7 +452,8 @@ namespace MWWorld Ogre::Vector3 to = newPosition - (physicActor->getOnGround() ? Ogre::Vector3(0,0,sStepSize+2.f) : Ogre::Vector3(0,0,2.f)); tracer.doTrace(colobj, from, to, engine); - if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope) + if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope + && tracer.mHitObject->getBroadphaseHandle()->m_collisionFilterGroup != OEngine::Physic::CollisionType_Actor) { const btCollisionObject* standingOn = tracer.mHitObject; if (const OEngine::Physic::RigidBody* body = dynamic_cast(standingOn)) @@ -468,7 +469,25 @@ namespace MWWorld isOnGround = true; } else + { + // standing on actors is not allowed (see above). + // in addition to that, apply a sliding effect away from the center of the actor, + // so that we do not stay suspended in air indefinitely. + if (tracer.mFraction < 1.0f && tracer.mHitObject->getBroadphaseHandle()->m_collisionFilterGroup == OEngine::Physic::CollisionType_Actor) + { + if (Ogre::Vector3(inertia.x, inertia.y, 0).squaredLength() < 100.f*100.f) + { + btVector3 aabbMin, aabbMax; + tracer.mHitObject->getCollisionShape()->getAabb(tracer.mHitObject->getWorldTransform(), aabbMin, aabbMax); + btVector3 center = (aabbMin + aabbMax) / 2.f; + inertia = Ogre::Vector3(position.x - center.x(), position.y - center.y(), 0); + inertia.normalise(); + inertia *= 100; + } + } + isOnGround = false; + } } if(isOnGround || newPosition.z < waterlevel || isFlying) From d642512f71698ff91c297d32a5412bf01f4ffbd6 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 17 Dec 2014 00:57:04 +0100 Subject: [PATCH 279/303] Error message fix --- apps/openmw/mwclass/creature.cpp | 5 ++--- apps/openmw/mwclass/npc.cpp | 11 ++++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 9d07aecf3f..5fd5f4dde0 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -120,11 +120,10 @@ namespace MWClass for (std::vector::const_iterator iter (ref->mBase->mSpells.mList.begin()); iter!=ref->mBase->mSpells.mList.end(); ++iter) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(*iter); - if (spell) + if (const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(*iter)) data->mCreatureStats.getSpells().add (spell); else /// \todo add option to make this a fatal error message pop-up, but default to warning for vanilla compatibility - std::cerr << "Warning: ignoring nonexistent spell '" << spell->mId << "' on creature '" << ref->mBase->mId << "'" << std::endl; + std::cerr << "Warning: ignoring nonexistent spell '" << *iter << "' on creature '" << ref->mBase->mId << "'" << std::endl; } // inventory diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index a3ebef5821..4814879acd 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -300,8 +300,7 @@ namespace MWClass if (!ref->mBase->mFaction.empty()) { std::string faction = ref->mBase->mFaction; - const ESM::Faction* fact = MWBase::Environment::get().getWorld()->getStore().get().search(faction); - if (fact) + if (const ESM::Faction* fact = MWBase::Environment::get().getWorld()->getStore().get().search(faction)) { if(ref->mBase->mNpdtType != ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) { @@ -313,7 +312,7 @@ namespace MWClass } } else - std::cerr << "Warning: ignoring nonexistent faction '" << fact->mId << "' on NPC '" << ref->mBase->mId << "'" << std::endl; + std::cerr << "Warning: ignoring nonexistent faction '" << faction << "' on NPC '" << ref->mBase->mId << "'" << std::endl; } // creature stats @@ -366,8 +365,7 @@ namespace MWClass for (std::vector::const_iterator iter (race->mPowers.mList.begin()); iter!=race->mPowers.mList.end(); ++iter) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(*iter); - if (spell) + if (const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(*iter)) data->mNpcStats.getSpells().add (spell); else std::cerr << "Warning: ignoring nonexistent race power '" << *iter << "' on NPC '" << ref->mBase->mId << "'" << std::endl; @@ -395,8 +393,7 @@ namespace MWClass for (std::vector::const_iterator iter (ref->mBase->mSpells.mList.begin()); iter!=ref->mBase->mSpells.mList.end(); ++iter) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(*iter); - if (spell) + if (const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(*iter)) data->mNpcStats.getSpells().add (spell); else { From 31d28e727f004d944c40c58e5f3822415192a265 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 17 Dec 2014 01:05:32 +0100 Subject: [PATCH 280/303] Implement leveled list script functions (Fixes #1546) --- apps/openmw/mwbase/world.hpp | 10 +++ apps/openmw/mwscript/docs/vmformat.txt | 6 +- apps/openmw/mwscript/miscextensions.cpp | 112 ++++++++++++++++++++++++ apps/openmw/mwstate/statemanagerimp.cpp | 2 + apps/openmw/mwworld/esmstore.cpp | 8 +- apps/openmw/mwworld/esmstore.hpp | 15 ++++ apps/openmw/mwworld/store.hpp | 10 +-- apps/openmw/mwworld/worldimp.cpp | 10 +++ apps/openmw/mwworld/worldimp.hpp | 8 ++ components/compiler/extensions0.cpp | 4 + components/compiler/opcodes.hpp | 4 + 11 files changed, 182 insertions(+), 7 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 34be298d3a..32beadf18a 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -40,6 +40,8 @@ namespace ESM struct Enchantment; struct Book; struct EffectList; + struct CreatureLevList; + struct ItemLevList; } namespace MWRender @@ -359,6 +361,14 @@ namespace MWBase ///< 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 void update (float duration, bool paused) = 0; virtual MWWorld::Ptr placeObject (const MWWorld::Ptr& object, float cursorX, float cursorY, int amount) = 0; diff --git a/apps/openmw/mwscript/docs/vmformat.txt b/apps/openmw/mwscript/docs/vmformat.txt index 800c6e2c7b..172e1b528a 100644 --- a/apps/openmw/mwscript/docs/vmformat.txt +++ b/apps/openmw/mwscript/docs/vmformat.txt @@ -438,5 +438,9 @@ 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 -opcodes 0x20002fb-0x3ffffff unused +opcodes 0x20002ff-0x3ffffff unused diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index c92acff820..f20c9967df 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -31,6 +31,42 @@ #include "interpretercontext.hpp" #include "ref.hpp" +namespace +{ + + void addToLevList(ESM::LeveledListBase* list, const std::string& itemId, int level) + { + for (std::vector::iterator it = list->mList.begin(); it != list->mList.end();) + { + if (it->mLevel == level && itemId == it->mId) + return; + } + + ESM::LeveledListBase::LevelItem item; + item.mId = itemId; + item.mLevel = level; + list->mList.push_back(item); + } + + void removeFromLevList(ESM::LeveledListBase* 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 @@ -1032,6 +1068,78 @@ namespace MWScript } }; + class OpAddToLevCreature : public Interpreter::Opcode0 + { + public: + virtual void execute(Interpreter::Runtime &runtime) + { + 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: + virtual void execute(Interpreter::Runtime &runtime) + { + 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: + virtual void execute(Interpreter::Runtime &runtime) + { + 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: + virtual void execute(Interpreter::Runtime &runtime) + { + 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); + } + }; + void installOpcodes (Interpreter::Interpreter& interpreter) { interpreter.installSegment5 (Compiler::Misc::opcodeXBox, new OpXBox); @@ -1121,6 +1229,10 @@ namespace MWScript interpreter.installSegment5 (Compiler::Misc::opcodeGetPcTraveling, new OpGetPcTraveling); interpreter.installSegment5 (Compiler::Misc::opcodeBetaComment, new OpBetaComment); interpreter.installSegment5 (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); } } } diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 18ebe11cee..9746202dc3 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -352,6 +352,8 @@ void MWState::StateManager::loadGame (const Character *character, const Slot *sl case ESM::REC_PROJ: case ESM::REC_MPRJ: case ESM::REC_ENAB: + case ESM::REC_LEVC: + case ESM::REC_LEVI: MWBase::Environment::get().getWorld()->readRecord (reader, n.val, contentFileMap); break; diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 56cb05c646..2a3fd9179e 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -149,7 +149,9 @@ void ESMStore::setUp() +mEnchants.getDynamicSize() +mNpcs.getDynamicSize() +mSpells.getDynamicSize() - +mWeapons.getDynamicSize(); + +mWeapons.getDynamicSize() + +mCreatureLists.getDynamicSize() + +mItemLists.getDynamicSize(); } void ESMStore::write (ESM::ESMWriter& writer, Loading::Listener& progress) const @@ -170,6 +172,8 @@ void ESMStore::setUp() mSpells.write (writer, progress); mWeapons.write (writer, progress); mNpcs.write (writer, progress); + mItemLists.write (writer, progress); + mCreatureLists.write (writer, progress); } bool ESMStore::readRecord (ESM::ESMReader& reader, int32_t type) @@ -185,6 +189,8 @@ void ESMStore::setUp() case ESM::REC_SPEL: case ESM::REC_WEAP: case ESM::REC_NPC_: + case ESM::REC_LEVI: + case ESM::REC_LEVC: mStores[type]->read (reader); diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index 83e911174d..5d794db895 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -167,6 +167,7 @@ namespace MWWorld 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) { std::ostringstream id; @@ -191,6 +192,20 @@ namespace MWWorld 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) { std::ostringstream id; diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index c4070f0327..a0d34b228d 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -145,17 +145,17 @@ namespace MWWorld T item; item.mId = Misc::StringUtils::lowerCase(id); + typename Dynamic::const_iterator dit = mDynamic.find(item.mId); + if (dit != mDynamic.end()) { + return &dit->second; + } + typename std::map::const_iterator it = mStatic.find(item.mId); if (it != mStatic.end() && Misc::StringUtils::ciEqual(it->second.mId, id)) { return &(it->second); } - typename Dynamic::const_iterator dit = mDynamic.find(item.mId); - if (dit != mDynamic.end()) { - return &dit->second; - } - return 0; } diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index e24e99c305..17a45f9f12 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1490,6 +1490,16 @@ namespace MWWorld 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::NPC *World::createRecord(const ESM::NPC &record) { bool update = false; diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index c8e254b2f9..2a0da917b9 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -420,6 +420,14 @@ namespace MWWorld ///< 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); + ///< 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); + ///< 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); virtual MWWorld::Ptr placeObject (const MWWorld::Ptr& object, float cursorX, float cursorY, int amount); diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index 690e589f73..d08a2bb320 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -308,6 +308,10 @@ namespace Compiler extensions.registerFunction ("getpctraveling", 'l', "", opcodeGetPcTraveling); extensions.registerInstruction ("betacomment", "S", opcodeBetaComment, opcodeBetaCommentExplicit); extensions.registerInstruction ("bc", "S", opcodeBetaComment, opcodeBetaCommentExplicit); + extensions.registerInstruction ("addtolevcreature", "ccl", opcodeAddToLevCreature); + extensions.registerInstruction ("removefromlevcreature", "ccl", opcodeRemoveFromLevCreature); + extensions.registerInstruction ("addtolevitem", "ccl", opcodeAddToLevItem); + extensions.registerInstruction ("removefromlevitem", "ccl", opcodeRemoveFromLevItem); } } diff --git a/components/compiler/opcodes.hpp b/components/compiler/opcodes.hpp index da79555e22..bbafd6b13b 100644 --- a/components/compiler/opcodes.hpp +++ b/components/compiler/opcodes.hpp @@ -284,6 +284,10 @@ namespace Compiler const int opcodeExplodeSpellExplicit = 0x200022a; const int opcodeGetPcInJail = 0x200023e; const int opcodeGetPcTraveling = 0x200023f; + const int opcodeAddToLevCreature = 0x20002fb; + const int opcodeRemoveFromLevCreature = 0x20002fc; + const int opcodeAddToLevItem = 0x20002fd; + const int opcodeRemoveFromLevItem = 0x20002fe; } namespace Sky From c2771bc8abf2024779d31afd998bf7f0b797b783 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 17 Dec 2014 02:15:40 +0100 Subject: [PATCH 281/303] Head tracking fix --- apps/openmw/mwrender/npcanimation.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 7c68a45380..3b0b4e08b2 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -626,10 +626,7 @@ Ogre::Vector3 NpcAnimation::runAnimation(float timepassed) Ogre::Node* node = baseinst->getBone("Bip01 Head"); if (node) - { - node->rotate(Ogre::Vector3::UNIT_Z, mHeadYaw, Ogre::Node::TS_WORLD); - node->rotate(Ogre::Vector3::UNIT_X, mHeadPitch, Ogre::Node::TS_WORLD); - } + node->rotate(Ogre::Quaternion(mHeadYaw, Ogre::Vector3::UNIT_Z) * Ogre::Quaternion(mHeadPitch, Ogre::Vector3::UNIT_X), Ogre::Node::TS_WORLD); } mFirstPersonOffset = 0.f; // reset the X, Y, Z offset for the next frame. From 899ae763e667bbe98be1ce459c1e169eb51a3764 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 17 Dec 2014 09:33:40 +0100 Subject: [PATCH 282/303] fixing a travis build problem --- apps/openmw/mwgui/itemview.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/openmw/mwgui/itemview.cpp b/apps/openmw/mwgui/itemview.cpp index 6af6a3b3f6..ed2002d72c 100644 --- a/apps/openmw/mwgui/itemview.cpp +++ b/apps/openmw/mwgui/itemview.cpp @@ -1,5 +1,7 @@ #include "itemview.hpp" +#include + #include #include From 5cb94da9c5a8491d724c9d4eb950e38e3b9c44fa Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 17 Dec 2014 11:56:54 +0100 Subject: [PATCH 283/303] compensate for incorrect minus character in translated dialogue script (Fixes #2207) --- components/compiler/scanner.cpp | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/components/compiler/scanner.cpp b/components/compiler/scanner.cpp index 203f27e6e8..488906d8ff 100644 --- a/components/compiler/scanner.cpp +++ b/components/compiler/scanner.cpp @@ -445,6 +445,32 @@ namespace Compiler else special = S_minus; } + else if (static_cast (c)==0xe2) + { + /// Workaround for some translator who apparently can't keep his minus in order + /// \todo disable for later script formats + if (get (c) && static_cast (c)==0x80 && + get (c) && static_cast (c)==0x93) + { + if (get (c)) + { + if (c=='>') + special = S_ref; + else + { + putback (c); + special = S_minus; + } + } + else + special = S_minus; + } + else + { + mErrorHandler.error ("Invalid character", mLoc); + return false; + } + } else if (c=='<') { if (get (c)) From b951251572b1cf0f1c473615422d66a6f640aa35 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 17 Dec 2014 15:03:05 +0100 Subject: [PATCH 284/303] handle junk in argument lists (Fixes #2206) --- components/CMakeLists.txt | 4 +-- components/compiler/exprparser.cpp | 11 ++++++- components/compiler/exprparser.hpp | 3 +- components/compiler/extensions.hpp | 1 + components/compiler/extensions0.cpp | 2 +- components/compiler/junkparser.cpp | 48 +++++++++++++++++++++++++++++ components/compiler/junkparser.hpp | 41 ++++++++++++++++++++++++ components/compiler/lineparser.cpp | 2 +- 8 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 components/compiler/junkparser.cpp create mode 100644 components/compiler/junkparser.hpp diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 234325718d..6918b87a7a 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -70,7 +70,7 @@ add_component_dir (compiler context controlparser errorhandler exception exprparser extensions fileparser generator lineparser literals locals output parser scanner scriptparser skipparser streamerrorhandler stringparser tokenloc nullerrorhandler opcodes extensions0 declarationparser - quickfileparser discardparser + quickfileparser discardparser junkparser ) add_component_dir (interpreter @@ -123,7 +123,7 @@ if(QT_QTGUI_LIBRARY AND QT_QTCORE_LIBRARY) launchersettings settingsbase ) - + add_component_qt_dir (process processinvoker ) diff --git a/components/compiler/exprparser.cpp b/components/compiler/exprparser.cpp index 6dcca08df9..18d9e6dc7a 100644 --- a/components/compiler/exprparser.cpp +++ b/components/compiler/exprparser.cpp @@ -17,6 +17,7 @@ #include "extensions.hpp" #include "context.hpp" #include "discardparser.hpp" +#include "junkparser.hpp" namespace Compiler { @@ -752,7 +753,7 @@ namespace Compiler } int ExprParser::parseArguments (const std::string& arguments, Scanner& scanner, - std::vector& code) + std::vector& code, int ignoreKeyword) { bool optional = false; int optionalCount = 0; @@ -760,6 +761,7 @@ namespace Compiler ExprParser parser (getErrorHandler(), getContext(), mLocals, mLiterals, true); StringParser stringParser (getErrorHandler(), getContext(), mLiterals); DiscardParser discardParser (getErrorHandler(), getContext()); + JunkParser junkParser (getErrorHandler(), getContext(), ignoreKeyword); std::stack > stack; @@ -815,6 +817,13 @@ namespace Compiler if (discardParser.isEmpty()) break; } + else if (*iter=='j') + { + /// \todo disable this when operating in strict mode + junkParser.reset(); + + scanner.scan (junkParser); + } else { parser.reset(); diff --git a/components/compiler/exprparser.hpp b/components/compiler/exprparser.hpp index e4e385ff0f..639ca65aab 100644 --- a/components/compiler/exprparser.hpp +++ b/components/compiler/exprparser.hpp @@ -96,11 +96,12 @@ namespace Compiler /// \return Type ('l': integer, 'f': float) int parseArguments (const std::string& arguments, Scanner& scanner, - std::vector& code); + std::vector& code, int ignoreKeyword = -1); ///< Parse sequence of arguments specified by \a arguments. /// \param arguments Uses ScriptArgs typedef /// \see Compiler::ScriptArgs /// \param invert Store arguments in reverted order. + /// \param ignoreKeyword A keyword that is seen as junk /// \return number of optional arguments }; } diff --git a/components/compiler/extensions.hpp b/components/compiler/extensions.hpp index a15218d99f..9fb9bdb95a 100644 --- a/components/compiler/extensions.hpp +++ b/components/compiler/extensions.hpp @@ -23,6 +23,7 @@ namespace Compiler x - Optional, ignored string argument X - Optional, ignored numeric expression z - Optional, ignored string or numeric argument + j - A piece of junk (either . or a specific keyword) **/ typedef std::string ScriptArgs; diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index d08a2bb320..234c5b12d4 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -179,7 +179,7 @@ namespace Compiler extensions.registerInstruction ("setjournalindex", "cl", opcodeSetJournalIndex); extensions.registerFunction ("getjournalindex", 'l', "c", opcodeGetJournalIndex); extensions.registerInstruction ("addtopic", "S" , opcodeAddTopic); - extensions.registerInstruction ("choice", "/SlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSl", opcodeChoice); + extensions.registerInstruction ("choice", "j/SlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSl", opcodeChoice); extensions.registerInstruction("forcegreeting","",opcodeForceGreeting, opcodeForceGreetingExplicit); extensions.registerInstruction("goodbye", "", opcodeGoodbye); diff --git a/components/compiler/junkparser.cpp b/components/compiler/junkparser.cpp new file mode 100644 index 0000000000..cfa94044e7 --- /dev/null +++ b/components/compiler/junkparser.cpp @@ -0,0 +1,48 @@ + +#include "junkparser.hpp" + +#include "scanner.hpp" + +Compiler::JunkParser::JunkParser (ErrorHandler& errorHandler, const Context& context, + int ignoreKeyword) +: Parser (errorHandler, context), mIgnoreKeyword (ignoreKeyword) +{} + +bool Compiler::JunkParser::parseInt (int value, const TokenLoc& loc, Scanner& scanner) +{ + scanner.putbackInt (value, loc); + return false; +} + +bool Compiler::JunkParser::parseFloat (float value, const TokenLoc& loc, Scanner& scanner) +{ + scanner.putbackFloat (value, loc); + return false; +} + +bool Compiler::JunkParser::parseName (const std::string& name, const TokenLoc& loc, + Scanner& scanner) +{ + scanner.putbackName (name, loc); + return false; +} + +bool Compiler::JunkParser::parseKeyword (int keyword, const TokenLoc& loc, Scanner& scanner) +{ + if (keyword==mIgnoreKeyword) + reportWarning ("found junk (ignoring it)", loc); + else + scanner.putbackKeyword (keyword, loc); + + return false; +} + +bool Compiler::JunkParser::parseSpecial (int code, const TokenLoc& loc, Scanner& scanner) +{ + if (code==Scanner::S_member) + reportWarning ("found junk (ignoring it)", loc); + else + scanner.putbackSpecial (code, loc); + + return false; +} diff --git a/components/compiler/junkparser.hpp b/components/compiler/junkparser.hpp new file mode 100644 index 0000000000..6dfbd97af4 --- /dev/null +++ b/components/compiler/junkparser.hpp @@ -0,0 +1,41 @@ +#ifndef COMPILER_JUNKPARSER_H_INCLUDED +#define COMPILER_JUNKPARSER_H_INCLUDED + +#include "parser.hpp" + +namespace Compiler +{ + /// \brief Parse an optional single junk token + class JunkParser : public Parser + { + int mIgnoreKeyword; + + public: + + JunkParser (ErrorHandler& errorHandler, const Context& context, + int ignoreKeyword = -1); + + virtual bool parseInt (int value, const TokenLoc& loc, Scanner& scanner); + ///< Handle an int token. + /// \return fetch another token? + + virtual bool parseFloat (float value, const TokenLoc& loc, Scanner& scanner); + ///< Handle a float token. + /// \return fetch another token? + + virtual bool parseName (const std::string& name, const TokenLoc& loc, + Scanner& scanner); + ///< Handle a name token. + /// \return fetch another token? + + virtual bool parseKeyword (int keyword, const TokenLoc& loc, Scanner& scanner); + ///< Handle a keyword token. + /// \return fetch another token? + + virtual bool parseSpecial (int code, const TokenLoc& loc, Scanner& scanner); + ///< Handle a special character token. + /// \return fetch another token? + }; +} + +#endif diff --git a/components/compiler/lineparser.cpp b/components/compiler/lineparser.cpp index dc19b9a4b0..2d08ed3fe7 100644 --- a/components/compiler/lineparser.cpp +++ b/components/compiler/lineparser.cpp @@ -304,7 +304,7 @@ namespace Compiler errorDowngrade.reset (new ErrorDowngrade (getErrorHandler())); std::vector code; - int optionals = mExprParser.parseArguments (argumentType, scanner, code); + int optionals = mExprParser.parseArguments (argumentType, scanner, code, keyword); mCode.insert (mCode.end(), code.begin(), code.end()); extensions->generateInstructionCode (keyword, mCode, mLiterals, mExplicit, optionals); From fc1d42a7d2d9b904092cfec7627c96f54a429022 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 18 Dec 2014 09:55:26 +0100 Subject: [PATCH 285/303] fixed exclusion for certain characters at the start of names --- components/compiler/scanner.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/compiler/scanner.cpp b/components/compiler/scanner.cpp index 59fae3ccdc..9acba861a9 100644 --- a/components/compiler/scanner.cpp +++ b/components/compiler/scanner.cpp @@ -314,7 +314,7 @@ namespace Compiler bool Scanner::scanName (char c, std::string& name) { - bool first = false; + bool first = true; bool error = false; name.clear(); From a6d30bc2e3014c77b883de9ceab40d141078f78f Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 18 Dec 2014 10:20:15 +0100 Subject: [PATCH 286/303] consider --script-warn when running with --script-all-dialogue --- apps/openmw/engine.cpp | 2 +- apps/openmw/mwdialogue/scripttest.cpp | 9 +++++---- apps/openmw/mwdialogue/scripttest.hpp | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 885d2bb4f1..6ea09d4c9d 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -436,7 +436,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) } if (mCompileAllDialogue) { - std::pair result = MWDialogue::ScriptTest::compileAll(&mExtensions); + std::pair result = MWDialogue::ScriptTest::compileAll(&mExtensions, mWarningsMode); if (result.first) std::cout << "compiled " << result.second << " of " << result.first << " dialogue script/actor combinations a(" diff --git a/apps/openmw/mwdialogue/scripttest.cpp b/apps/openmw/mwdialogue/scripttest.cpp index a8de21ef97..b2c8f536a2 100644 --- a/apps/openmw/mwdialogue/scripttest.cpp +++ b/apps/openmw/mwdialogue/scripttest.cpp @@ -21,7 +21,7 @@ namespace { -void test(const MWWorld::Ptr& actor, int &compiled, int &total, const Compiler::Extensions* extensions) +void test(const MWWorld::Ptr& actor, int &compiled, int &total, const Compiler::Extensions* extensions, int warningsMode) { MWDialogue::Filter filter(actor, 0, false); @@ -29,6 +29,7 @@ void test(const MWWorld::Ptr& actor, int &compiled, int &total, const Compiler:: compilerContext.setExtensions(extensions); std::ostream errorStream(std::cout.rdbuf()); Compiler::StreamErrorHandler errorHandler(errorStream); + errorHandler.setWarningsMode (warningsMode); const MWWorld::Store& dialogues = MWBase::Environment::get().getWorld()->getStore().get(); for (MWWorld::Store::iterator it = dialogues.begin(); it != dialogues.end(); ++it) @@ -100,21 +101,21 @@ namespace MWDialogue namespace ScriptTest { - std::pair compileAll(const Compiler::Extensions *extensions) + 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); + 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); + test(ref.getPtr(), compiled, total, extensions, warningsMode); } return std::make_pair(total, compiled); } diff --git a/apps/openmw/mwdialogue/scripttest.hpp b/apps/openmw/mwdialogue/scripttest.hpp index 1ed94c76a5..0ac2597256 100644 --- a/apps/openmw/mwdialogue/scripttest.hpp +++ b/apps/openmw/mwdialogue/scripttest.hpp @@ -11,7 +11,7 @@ namespace ScriptTest /// Attempt to compile all dialogue scripts, use for verification purposes /// @return A pair containing -std::pair compileAll(const Compiler::Extensions* extensions); +std::pair compileAll(const Compiler::Extensions* extensions, int warningsMode); } From 120873a66d7607446c56d7338f81ae0536844d31 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Thu, 18 Dec 2014 10:40:51 +0100 Subject: [PATCH 287/303] another workaround for script translation messup --- components/compiler/scanner.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/compiler/scanner.cpp b/components/compiler/scanner.cpp index 2aeaffd9c3..3fdbdb9f01 100644 --- a/components/compiler/scanner.cpp +++ b/components/compiler/scanner.cpp @@ -545,7 +545,7 @@ namespace Compiler { return std::isalpha (c) || std::isdigit (c) || c=='_' || /// \todo disable this when doing more stricter compiling - c=='`' || + c=='`' || c=='\'' || /// \todo disable this when doing more stricter compiling. Also, find out who is /// responsible for allowing it in the first place and meet up with that person in /// a dark alley. From 0af5c7b3798efdf18870cae513e72ae4ae864a48 Mon Sep 17 00:00:00 2001 From: "Alexander \"Ace\" Olofsson" Date: Fri, 19 Dec 2014 09:23:16 +0100 Subject: [PATCH 288/303] Starting to clean up some heavy includes --- apps/openmw/mwgui/widgets.cpp | 1 + apps/openmw/mwgui/widgets.hpp | 4 +++- apps/openmw/mwmechanics/aipackage.hpp | 1 - apps/openmw/mwscript/interpretercontext.hpp | 2 -- apps/openmw/mwworld/cellstore.hpp | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/openmw/mwgui/widgets.cpp b/apps/openmw/mwgui/widgets.cpp index d7bc2c96e9..052407c581 100644 --- a/apps/openmw/mwgui/widgets.cpp +++ b/apps/openmw/mwgui/widgets.cpp @@ -1,4 +1,5 @@ #include "widgets.hpp" +#include "../mwworld/esmstore.hpp" #include diff --git a/apps/openmw/mwgui/widgets.hpp b/apps/openmw/mwgui/widgets.hpp index aae28a6862..6ce114131e 100644 --- a/apps/openmw/mwgui/widgets.hpp +++ b/apps/openmw/mwgui/widgets.hpp @@ -1,10 +1,12 @@ #ifndef MWGUI_WIDGETS_H #define MWGUI_WIDGETS_H -#include "../mwworld/esmstore.hpp" #include "../mwmechanics/stat.hpp" #include "controllers.hpp" +#include +#include + #include #include #include diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index f1c9ec7d25..ca08de072c 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -3,7 +3,6 @@ #include "pathfinding.hpp" #include -#include "../mwbase/world.hpp" #include "obstacle.hpp" #include "aistate.hpp" diff --git a/apps/openmw/mwscript/interpretercontext.hpp b/apps/openmw/mwscript/interpretercontext.hpp index 354df00bdf..698df62c2a 100644 --- a/apps/openmw/mwscript/interpretercontext.hpp +++ b/apps/openmw/mwscript/interpretercontext.hpp @@ -5,8 +5,6 @@ #include -#include "../mwbase/world.hpp" - #include "../mwworld/ptr.hpp" #include "../mwworld/action.hpp" diff --git a/apps/openmw/mwworld/cellstore.hpp b/apps/openmw/mwworld/cellstore.hpp index eba627b3ee..f6f2a3b489 100644 --- a/apps/openmw/mwworld/cellstore.hpp +++ b/apps/openmw/mwworld/cellstore.hpp @@ -6,10 +6,10 @@ #include #include "livecellref.hpp" -#include "esmstore.hpp" #include "cellreflist.hpp" #include +#include #include "../mwmechanics/pathgrid.hpp" // TODO: maybe belongs in mwworld @@ -24,7 +24,7 @@ namespace ESM namespace MWWorld { class Ptr; - + class ESMStore; /// \brief Mutable state of a cell From 462b41a3a8a6bd63fcf35781aa0e59e6754c988e Mon Sep 17 00:00:00 2001 From: "Alexander \"Ace\" Olofsson" Date: Fri, 19 Dec 2014 11:26:54 +0100 Subject: [PATCH 289/303] Missing files, aka; Why you shouldn't stresscommit --- apps/openmw/mwclass/activator.cpp | 3 ++- apps/openmw/mwclass/armor.cpp | 1 + apps/openmw/mwclass/book.cpp | 1 + apps/openmw/mwclass/clothing.cpp | 1 + apps/openmw/mwclass/container.cpp | 1 + apps/openmw/mwclass/door.cpp | 1 + apps/openmw/mwclass/ingredient.cpp | 1 + apps/openmw/mwclass/potion.cpp | 1 + apps/openmw/mwclass/weapon.cpp | 1 + apps/openmw/mwgui/alchemywindow.cpp | 3 +++ apps/openmw/mwgui/birth.cpp | 2 ++ apps/openmw/mwgui/charactercreation.cpp | 1 + apps/openmw/mwgui/class.cpp | 1 + apps/openmw/mwgui/class.hpp | 3 ++- apps/openmw/mwgui/console.cpp | 1 + apps/openmw/mwgui/dialogue.cpp | 1 + apps/openmw/mwgui/enchantingdialog.cpp | 2 ++ apps/openmw/mwgui/hud.cpp | 2 ++ apps/openmw/mwgui/mapwindow.cpp | 1 + apps/openmw/mwgui/quickkeysmenu.cpp | 1 + apps/openmw/mwgui/race.cpp | 1 + apps/openmw/mwgui/recharge.cpp | 3 +++ apps/openmw/mwgui/review.cpp | 1 + apps/openmw/mwgui/review.hpp | 2 ++ apps/openmw/mwgui/savegamedialog.cpp | 1 + apps/openmw/mwgui/spellcreationdialog.cpp | 2 ++ apps/openmw/mwgui/spellcreationdialog.hpp | 2 ++ apps/openmw/mwgui/spellicons.cpp | 2 ++ apps/openmw/mwgui/spellwindow.cpp | 1 + apps/openmw/mwgui/tooltips.cpp | 1 + apps/openmw/mwgui/tooltips.hpp | 6 ++++++ apps/openmw/mwgui/trainingwindow.cpp | 1 + apps/openmw/mwgui/waitdialog.cpp | 1 + apps/openmw/mwmechanics/pathgrid.cpp | 1 + apps/openmw/mwmechanics/spellcasting.cpp | 1 + apps/openmw/mwscript/skyextensions.cpp | 1 + apps/openmw/mwstate/statemanagerimp.cpp | 1 + apps/openmw/mwworld/class.cpp | 1 + 38 files changed, 56 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwclass/activator.cpp b/apps/openmw/mwclass/activator.cpp index bf02b4d05e..46b23e942d 100644 --- a/apps/openmw/mwclass/activator.cpp +++ b/apps/openmw/mwclass/activator.cpp @@ -8,7 +8,8 @@ #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" -#include "../mwworld//cellstore.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwworld/action.hpp" diff --git a/apps/openmw/mwclass/armor.cpp b/apps/openmw/mwclass/armor.cpp index 97f9211d9a..2fa6602c4d 100644 --- a/apps/openmw/mwclass/armor.cpp +++ b/apps/openmw/mwclass/armor.cpp @@ -14,6 +14,7 @@ #include "../mwworld/actionequip.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwworld/nullaction.hpp" #include "../mwworld/containerstore.hpp" diff --git a/apps/openmw/mwclass/book.cpp b/apps/openmw/mwclass/book.cpp index 51d47e7216..b99d71a06f 100644 --- a/apps/openmw/mwclass/book.cpp +++ b/apps/openmw/mwclass/book.cpp @@ -11,6 +11,7 @@ #include "../mwworld/actionread.hpp" #include "../mwworld/failedaction.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwrender/objects.hpp" diff --git a/apps/openmw/mwclass/clothing.cpp b/apps/openmw/mwclass/clothing.cpp index 0098783505..eb2dec0ab2 100644 --- a/apps/openmw/mwclass/clothing.cpp +++ b/apps/openmw/mwclass/clothing.cpp @@ -12,6 +12,7 @@ #include "../mwworld/actionequip.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwworld/nullaction.hpp" diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index 59e51e4613..3430dcb075 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -15,6 +15,7 @@ #include "../mwworld/containerstore.hpp" #include "../mwworld/customdata.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/actionopen.hpp" #include "../mwworld/actiontrap.hpp" #include "../mwworld/physicssystem.hpp" diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index fa9db9e160..8dc135ec84 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -15,6 +15,7 @@ #include "../mwworld/actionteleport.hpp" #include "../mwworld/actiondoor.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/actiontrap.hpp" diff --git a/apps/openmw/mwclass/ingredient.cpp b/apps/openmw/mwclass/ingredient.cpp index fa03f23ff1..610a0b478c 100644 --- a/apps/openmw/mwclass/ingredient.cpp +++ b/apps/openmw/mwclass/ingredient.cpp @@ -10,6 +10,7 @@ #include "../mwworld/ptr.hpp" #include "../mwworld/actiontake.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwworld/actioneat.hpp" #include "../mwworld/nullaction.hpp" diff --git a/apps/openmw/mwclass/potion.cpp b/apps/openmw/mwclass/potion.cpp index 2da213c8d6..814d903ffb 100644 --- a/apps/openmw/mwclass/potion.cpp +++ b/apps/openmw/mwclass/potion.cpp @@ -11,6 +11,7 @@ #include "../mwworld/actiontake.hpp" #include "../mwworld/actionapply.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwworld/nullaction.hpp" diff --git a/apps/openmw/mwclass/weapon.cpp b/apps/openmw/mwclass/weapon.cpp index d2f88efef3..f1f0386c64 100644 --- a/apps/openmw/mwclass/weapon.cpp +++ b/apps/openmw/mwclass/weapon.cpp @@ -12,6 +12,7 @@ #include "../mwworld/actionequip.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwworld/nullaction.hpp" diff --git a/apps/openmw/mwgui/alchemywindow.cpp b/apps/openmw/mwgui/alchemywindow.cpp index b9e0044cef..bb201e2dd6 100644 --- a/apps/openmw/mwgui/alchemywindow.cpp +++ b/apps/openmw/mwgui/alchemywindow.cpp @@ -11,6 +11,9 @@ #include "../mwmechanics/magiceffects.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" + +#include #include "inventoryitemmodel.hpp" #include "sortfilteritemmodel.hpp" diff --git a/apps/openmw/mwgui/birth.cpp b/apps/openmw/mwgui/birth.cpp index a7f90c00ba..949452a913 100644 --- a/apps/openmw/mwgui/birth.cpp +++ b/apps/openmw/mwgui/birth.cpp @@ -2,11 +2,13 @@ #include +#include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwworld/esmstore.hpp" #include "widgets.hpp" diff --git a/apps/openmw/mwgui/charactercreation.cpp b/apps/openmw/mwgui/charactercreation.cpp index 85f57a1a8c..33d0c4907d 100644 --- a/apps/openmw/mwgui/charactercreation.cpp +++ b/apps/openmw/mwgui/charactercreation.cpp @@ -13,6 +13,7 @@ #include "../mwmechanics/npcstats.hpp" #include "../mwworld/class.hpp" #include "../mwworld/fallback.hpp" +#include "../mwworld/esmstore.hpp" namespace { diff --git a/apps/openmw/mwgui/class.cpp b/apps/openmw/mwgui/class.cpp index 4e45f1a7c5..89c7346be1 100644 --- a/apps/openmw/mwgui/class.cpp +++ b/apps/openmw/mwgui/class.cpp @@ -3,6 +3,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwworld/esmstore.hpp" #include "tooltips.hpp" diff --git a/apps/openmw/mwgui/class.hpp b/apps/openmw/mwgui/class.hpp index 40056ca5e9..9d529ece01 100644 --- a/apps/openmw/mwgui/class.hpp +++ b/apps/openmw/mwgui/class.hpp @@ -1,7 +1,8 @@ #ifndef MWGUI_CLASS_H #define MWGUI_CLASS_H - +#include +#include #include "widgets.hpp" #include "windowbase.hpp" diff --git a/apps/openmw/mwgui/console.cpp b/apps/openmw/mwgui/console.cpp index 5a7193d650..c922b625d7 100644 --- a/apps/openmw/mwgui/console.cpp +++ b/apps/openmw/mwgui/console.cpp @@ -10,6 +10,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index eb548d596d..787e4e36eb 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -15,6 +15,7 @@ #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwdialogue/dialoguemanagerimp.hpp" diff --git a/apps/openmw/mwgui/enchantingdialog.cpp b/apps/openmw/mwgui/enchantingdialog.cpp index 56caa6513b..59bcd96475 100644 --- a/apps/openmw/mwgui/enchantingdialog.cpp +++ b/apps/openmw/mwgui/enchantingdialog.cpp @@ -3,6 +3,7 @@ #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -11,6 +12,7 @@ #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" +#include "../mwworld/esmstore.hpp" #include "itemselection.hpp" #include "container.hpp" diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index 2593e6e77c..d2d2fe51bb 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -7,8 +7,10 @@ #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.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" diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index 0262c4d0e4..db2a2dd75f 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -11,6 +11,7 @@ #include "../mwworld/player.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwrender/globalmap.hpp" diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 7f56b046e2..d59b29f0fc 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -8,6 +8,7 @@ #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" diff --git a/apps/openmw/mwgui/race.cpp b/apps/openmw/mwgui/race.cpp index bcb766f8f3..e60c667478 100644 --- a/apps/openmw/mwgui/race.cpp +++ b/apps/openmw/mwgui/race.cpp @@ -3,6 +3,7 @@ #include #include +#include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" diff --git a/apps/openmw/mwgui/recharge.cpp b/apps/openmw/mwgui/recharge.cpp index c45c2566ea..7458a6effb 100644 --- a/apps/openmw/mwgui/recharge.cpp +++ b/apps/openmw/mwgui/recharge.cpp @@ -3,12 +3,15 @@ #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/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" diff --git a/apps/openmw/mwgui/review.cpp b/apps/openmw/mwgui/review.cpp index d1f8a76a62..ca1b6ed5d7 100644 --- a/apps/openmw/mwgui/review.cpp +++ b/apps/openmw/mwgui/review.cpp @@ -5,6 +5,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwworld/esmstore.hpp" #include "tooltips.hpp" diff --git a/apps/openmw/mwgui/review.hpp b/apps/openmw/mwgui/review.hpp index 01b106d907..1419925b59 100644 --- a/apps/openmw/mwgui/review.hpp +++ b/apps/openmw/mwgui/review.hpp @@ -1,6 +1,8 @@ #ifndef MWGUI_REVIEW_H #define MWGUI_REVIEW_H +#include +#include #include "windowbase.hpp" #include "widgets.hpp" diff --git a/apps/openmw/mwgui/savegamedialog.cpp b/apps/openmw/mwgui/savegamedialog.cpp index 66c7a92386..6c8c7d737d 100644 --- a/apps/openmw/mwgui/savegamedialog.cpp +++ b/apps/openmw/mwgui/savegamedialog.cpp @@ -12,6 +12,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwstate/character.hpp" diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 5da33c67ae..fd0c4ad323 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -3,6 +3,7 @@ #include #include +#include #include "../mwbase/windowmanager.hpp" #include "../mwbase/soundmanager.hpp" @@ -12,6 +13,7 @@ #include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/spells.hpp" diff --git a/apps/openmw/mwgui/spellcreationdialog.hpp b/apps/openmw/mwgui/spellcreationdialog.hpp index ecccf3baf9..a94289bfdc 100644 --- a/apps/openmw/mwgui/spellcreationdialog.hpp +++ b/apps/openmw/mwgui/spellcreationdialog.hpp @@ -1,6 +1,8 @@ #ifndef MWGUI_SPELLCREATION_H #define MWGUI_SPELLCREATION_H +#include +#include #include #include "windowbase.hpp" diff --git a/apps/openmw/mwgui/spellicons.cpp b/apps/openmw/mwgui/spellicons.cpp index dbd91ab753..9d55c9e0e0 100644 --- a/apps/openmw/mwgui/spellicons.cpp +++ b/apps/openmw/mwgui/spellicons.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include "../mwbase/world.hpp" @@ -12,6 +13,7 @@ #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwmechanics/creaturestats.hpp" diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index 240d0419e3..98ee588b19 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -8,6 +8,7 @@ #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/spells.hpp" diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index 396c8fa489..aafea11780 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -11,6 +11,7 @@ #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/spellcasting.hpp" #include "mapwindow.hpp" diff --git a/apps/openmw/mwgui/tooltips.hpp b/apps/openmw/mwgui/tooltips.hpp index ffbb35e047..21b5527ccb 100644 --- a/apps/openmw/mwgui/tooltips.hpp +++ b/apps/openmw/mwgui/tooltips.hpp @@ -7,6 +7,12 @@ #include "widgets.hpp" +namespace ESM +{ + class Class; + struct Race; +} + namespace MWGui { // Info about tooltip that is supplied by the MWWorld::Class object diff --git a/apps/openmw/mwgui/trainingwindow.cpp b/apps/openmw/mwgui/trainingwindow.cpp index 6ff5ae35f6..56c9dff98d 100644 --- a/apps/openmw/mwgui/trainingwindow.cpp +++ b/apps/openmw/mwgui/trainingwindow.cpp @@ -10,6 +10,7 @@ #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/npcstats.hpp" diff --git a/apps/openmw/mwgui/waitdialog.cpp b/apps/openmw/mwgui/waitdialog.cpp index f95ec56751..f2f4a1f91f 100644 --- a/apps/openmw/mwgui/waitdialog.cpp +++ b/apps/openmw/mwgui/waitdialog.cpp @@ -12,6 +12,7 @@ #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" diff --git a/apps/openmw/mwmechanics/pathgrid.cpp b/apps/openmw/mwmechanics/pathgrid.cpp index 848d2c7a03..9e50af2b8c 100644 --- a/apps/openmw/mwmechanics/pathgrid.cpp +++ b/apps/openmw/mwmechanics/pathgrid.cpp @@ -4,6 +4,7 @@ #include "../mwbase/environment.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" namespace { diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index d33bb6ad15..6984d4c783 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -15,6 +15,7 @@ #include "../mwworld/player.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwrender/animation.hpp" diff --git a/apps/openmw/mwscript/skyextensions.cpp b/apps/openmw/mwscript/skyextensions.cpp index 8b9efd74e1..0ccd0ce311 100644 --- a/apps/openmw/mwscript/skyextensions.cpp +++ b/apps/openmw/mwscript/skyextensions.cpp @@ -9,6 +9,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" #include "interpretercontext.hpp" diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 9746202dc3..f77a90d8ee 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -27,6 +27,7 @@ #include "../mwworld/player.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwmechanics/npcstats.hpp" diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index 7c95858343..61c597517a 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -10,6 +10,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwworld/esmstore.hpp" #include "ptr.hpp" #include "refdata.hpp" From c7965894205f8e3e12a92dee22927af817ccc209 Mon Sep 17 00:00:00 2001 From: MiroslavR Date: Mon, 22 Dec 2014 01:54:24 +0100 Subject: [PATCH 290/303] Allow adding multiple Attribute/Skill effects in spell making (Fixes #2224) --- apps/openmw/mwgui/spellcreationdialog.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 5da33c67ae..3c9de90211 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -592,14 +592,6 @@ namespace MWGui int buttonId = *sender->getUserData(); mSelectedKnownEffectId = mButtonMapping[buttonId]; - for (std::vector::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) - { - if (it->mEffectID == mSelectedKnownEffectId) - { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sOnetypeEffectMessage}"); - return; - } - } const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld()->getStore().get().find(mSelectedKnownEffectId); @@ -622,6 +614,15 @@ namespace MWGui } else { + for (std::vector::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) + { + if (it->mEffectID == mSelectedKnownEffectId) + { + MWBase::Environment::get().getWindowManager()->messageBox ("#{sOnetypeEffectMessage}"); + return; + } + } + mAddEffectDialog.newEffect(effect); } } From ec18a2cfa0e309cf517218183da9e35262795a2d Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Mon, 22 Dec 2014 02:16:30 +0100 Subject: [PATCH 291/303] add support for borderless windows --- files/settings-default.cfg | 1 + libs/openengine/ogre/renderer.cpp | 4 +++- libs/openengine/ogre/renderer.hpp | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 7566994e29..25959546ed 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -6,6 +6,7 @@ resolution x = 800 resolution y = 600 fullscreen = false +borderless = true screen = 0 # Minimize the window if it loses key focus? diff --git a/libs/openengine/ogre/renderer.cpp b/libs/openengine/ogre/renderer.cpp index eb33d5876c..fadfc11a64 100644 --- a/libs/openengine/ogre/renderer.cpp +++ b/libs/openengine/ogre/renderer.cpp @@ -128,7 +128,9 @@ void OgreRenderer::createWindow(const std::string &title, const WindowSettings& settings.window_x, // width, in pixels settings.window_y, // height, in pixels SDL_WINDOW_SHOWN - | (settings.fullscreen ? SDL_WINDOW_FULLSCREEN : 0) | SDL_WINDOW_RESIZABLE + | SDL_WINDOW_RESIZABLE + | (settings.fullscreen ? SDL_WINDOW_FULLSCREEN : 0) + | (settings.borderless ? SDL_WINDOW_BORDERLESS : 0) ); SFO::SDLWindowHelper helper(mSDLWindow, settings.window_x, settings.window_y, title, settings.fullscreen, params); diff --git a/libs/openengine/ogre/renderer.hpp b/libs/openengine/ogre/renderer.hpp index e56f5f8165..5f1f6a4bb2 100644 --- a/libs/openengine/ogre/renderer.hpp +++ b/libs/openengine/ogre/renderer.hpp @@ -37,6 +37,7 @@ namespace OEngine { bool vsync; bool fullscreen; + bool borderless; int window_x, window_y; int screen; std::string fsaa; From 639fbfad0b55d5b7a606b6ed847a414dd33dfe34 Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Mon, 22 Dec 2014 02:44:20 +0100 Subject: [PATCH 292/303] make borderless setting available to the UI --- apps/launcher/graphicspage.cpp | 6 ++++++ apps/openmw/engine.cpp | 1 + apps/openmw/mwgui/settingswindow.cpp | 1 + apps/openmw/mwgui/settingswindow.hpp | 1 + files/mygui/openmw_settings_window.layout | 10 ++++++++++ files/ui/graphicspage.ui | 17 ++++++++++++----- 6 files changed, 31 insertions(+), 5 deletions(-) diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index ec7f5a04d7..11132f0f47 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -159,6 +159,9 @@ bool Launcher::GraphicsPage::loadSettings() if (mGraphicsSettings.value(QString("Video/fullscreen")) == QLatin1String("true")) fullScreenCheckBox->setCheckState(Qt::Checked); + if (mGraphicsSettings.value(QString("Video/borderless")) == QLatin1String("true")) + borderlessCheckBox->setCheckState(Qt::Checked); + int aaIndex = antiAliasingComboBox->findText(mGraphicsSettings.value(QString("Video/antialiasing"))); if (aaIndex != -1) antiAliasingComboBox->setCurrentIndex(aaIndex); @@ -193,6 +196,9 @@ void Launcher::GraphicsPage::saveSettings() fullScreenCheckBox->checkState() ? mGraphicsSettings.setValue(QString("Video/fullscreen"), QString("true")) : mGraphicsSettings.setValue(QString("Video/fullscreen"), QString("false")); + borderlessCheckBox->checkState() ? mGraphicsSettings.setValue(QString("Video/borderless"), QString("true")) + : mGraphicsSettings.setValue(QString("Video/borderless"), QString("false")); + mGraphicsSettings.setValue(QString("Video/antialiasing"), antiAliasingComboBox->currentText()); mGraphicsSettings.setValue(QString("Video/render system"), rendererComboBox->currentText()); diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 3d8aa5530b..85127db284 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -350,6 +350,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) OEngine::Render::WindowSettings windowSettings; windowSettings.fullscreen = settings.getBool("fullscreen", "Video"); + windowSettings.borderless = settings.getBool("borderless", "Video"); windowSettings.window_x = settings.getInt("resolution x", "Video"); windowSettings.window_y = settings.getInt("resolution y", "Video"); windowSettings.screen = settings.getInt("screen", "Video"); diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 8ef0a331a4..78a46cb6aa 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -166,6 +166,7 @@ namespace MWGui getWidget(mResolutionList, "ResolutionList"); getWidget(mFullscreenButton, "FullscreenButton"); getWidget(mVSyncButton, "VSyncButton"); + getWidget(mBorderlessButton, "BorderlessButton"); getWidget(mFPSButton, "FPSButton"); getWidget(mFOVSlider, "FOVSlider"); getWidget(mAnisotropySlider, "AnisotropySlider"); diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp index 2619943f98..9214fbd6f9 100644 --- a/apps/openmw/mwgui/settingswindow.hpp +++ b/apps/openmw/mwgui/settingswindow.hpp @@ -28,6 +28,7 @@ namespace MWGui MyGUI::ListBox* mResolutionList; MyGUI::Button* mFullscreenButton; MyGUI::Button* mVSyncButton; + MyGUI::Button* mBorderlessButton; MyGUI::Button* mFPSButton; MyGUI::ScrollBar* mFOVSlider; MyGUI::ScrollBar* mDifficultySlider; diff --git a/files/mygui/openmw_settings_window.layout b/files/mygui/openmw_settings_window.layout index fd84be3e9e..5be873d587 100644 --- a/files/mygui/openmw_settings_window.layout +++ b/files/mygui/openmw_settings_window.layout @@ -239,6 +239,16 @@ + + + + + + + + + + diff --git a/files/ui/graphicspage.ui b/files/ui/graphicspage.ui index 7e9fe00d9e..753d65d033 100644 --- a/files/ui/graphicspage.ui +++ b/files/ui/graphicspage.ui @@ -51,20 +51,27 @@ + + + Borderless + + + + Anti-aliasing: - + Screen: - + Resolution: @@ -74,13 +81,13 @@ - + - + - + From ebd384455f6a3f295b5dbf85db7bb3703a2a4c5b Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Mon, 22 Dec 2014 02:56:07 +0100 Subject: [PATCH 293/303] disable borderless window by default fix positioning of the in-game graphics settings --- files/mygui/openmw_settings_window.layout | 8 ++++---- files/settings-default.cfg | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/files/mygui/openmw_settings_window.layout b/files/mygui/openmw_settings_window.layout index 5be873d587..6e5d8b9e38 100644 --- a/files/mygui/openmw_settings_window.layout +++ b/files/mygui/openmw_settings_window.layout @@ -239,7 +239,7 @@ - + @@ -249,13 +249,13 @@ - + - + @@ -265,7 +265,7 @@ - + diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 25959546ed..69a9b2db5c 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -6,7 +6,7 @@ resolution x = 800 resolution y = 600 fullscreen = false -borderless = true +borderless = false screen = 0 # Minimize the window if it loses key focus? From dcfadeb51a695afe0041b78eff814719c40ecf66 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Mon, 22 Dec 2014 10:45:34 +0100 Subject: [PATCH 294/303] fix typo and annoying gcc/clang unused return values in crash catcher --- apps/openmw/crashcatcher.cpp | 6 +++--- apps/wizard/installationpage.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/openmw/crashcatcher.cpp b/apps/openmw/crashcatcher.cpp index b9d78540e0..2426714d33 100644 --- a/apps/openmw/crashcatcher.cpp +++ b/apps/openmw/crashcatcher.cpp @@ -11,7 +11,6 @@ #include #include - #include #include #include @@ -29,6 +28,7 @@ #include #endif +#define UNUSED(x) (void)(x) static const char crash_switch[] = "--cc-handle-crash"; @@ -160,7 +160,7 @@ static void gdb_info(pid_t pid) printf("Executing: %s\n", cmd_buf); fflush(stdout); - system(cmd_buf); + int unused = system(cmd_buf); UNUSED(unused); /* Clean up */ remove(respfile); } @@ -406,7 +406,7 @@ int cc_install_handlers(int argc, char **argv, int num_signals, int *signals, co snprintf(argv0, sizeof(argv0), "%s", argv[0]); else { - getcwd(argv0, sizeof(argv0)); + char * unused = getcwd(argv0, sizeof(argv0)); UNUSED(unused); retval = strlen(argv0); snprintf(argv0+retval, sizeof(argv0)-retval, "/%s", argv[0]); } diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 09e5773173..dc2674680e 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -185,7 +185,7 @@ void Wizard::InstallationPage::installationFinished() msgBox.setWindowTitle(tr("Installation finished")); msgBox.setIcon(QMessageBox::Information); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("Installation completed sucessfully!")); + msgBox.setText(tr("Installation completed successfully!")); msgBox.exec(); From 45299abe99f82f27033afd2b9f309d9611ee38ac Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 23 Dec 2014 17:13:11 +0100 Subject: [PATCH 295/303] make it C98 compat --- apps/openmw/crashcatcher.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/openmw/crashcatcher.cpp b/apps/openmw/crashcatcher.cpp index 2426714d33..8f25d041cb 100644 --- a/apps/openmw/crashcatcher.cpp +++ b/apps/openmw/crashcatcher.cpp @@ -160,7 +160,11 @@ static void gdb_info(pid_t pid) printf("Executing: %s\n", cmd_buf); fflush(stdout); - int unused = system(cmd_buf); UNUSED(unused); + { /* another special exception for "ignoring return value..." */ + int unused; + unused = system(cmd_buf); + UNUSED(unused); + } /* Clean up */ remove(respfile); } @@ -406,7 +410,13 @@ int cc_install_handlers(int argc, char **argv, int num_signals, int *signals, co snprintf(argv0, sizeof(argv0), "%s", argv[0]); else { - char * unused = getcwd(argv0, sizeof(argv0)); UNUSED(unused); + { + /* we don't want to disable "ignoring return value" warnings, so we make + * a special exception here. */ + char * unused; + unused = getcwd(argv0, sizeof(argv0)); + UNUSED(unused); + } retval = strlen(argv0); snprintf(argv0+retval, sizeof(argv0)-retval, "/%s", argv[0]); } From 3cc32b641ae6ff1ba06599b84940aaad1c3f529a Mon Sep 17 00:00:00 2001 From: MiroslavR Date: Tue, 23 Dec 2014 20:44:25 +0100 Subject: [PATCH 296/303] Fix some memory leaks --- apps/launcher/utils/profilescombobox.cpp | 6 +++--- apps/launcher/utils/textinputdialog.cpp | 6 +++--- .../contentselector/model/contentmodel.cpp | 16 ++++++++++++---- .../contentselector/model/contentmodel.hpp | 1 + .../contentselector/view/contentselector.cpp | 2 +- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/apps/launcher/utils/profilescombobox.cpp b/apps/launcher/utils/profilescombobox.cpp index c143307249..7df89098e2 100644 --- a/apps/launcher/utils/profilescombobox.cpp +++ b/apps/launcher/utils/profilescombobox.cpp @@ -47,13 +47,13 @@ void ProfilesComboBox::setEditEnabled(bool editable) void ProfilesComboBox::slotTextChanged(const QString &text) { - QPalette *palette = new QPalette(); - palette->setColor(QPalette::Text,Qt::red); + QPalette palette; + palette.setColor(QPalette::Text,Qt::red); int index = findText(text); if (text.isEmpty() || (index != -1 && index != currentIndex())) { - lineEdit()->setPalette(*palette); + lineEdit()->setPalette(palette); } else { lineEdit()->setPalette(QApplication::palette()); } diff --git a/apps/launcher/utils/textinputdialog.cpp b/apps/launcher/utils/textinputdialog.cpp index b47fdb6e62..385d086fd0 100644 --- a/apps/launcher/utils/textinputdialog.cpp +++ b/apps/launcher/utils/textinputdialog.cpp @@ -59,13 +59,13 @@ void Launcher::TextInputDialog::setOkButtonEnabled(bool enabled) QPushButton *okButton = mButtonBox->button(QDialogButtonBox::Ok); okButton->setEnabled(enabled); - QPalette *palette = new QPalette(); - palette->setColor(QPalette::Text, Qt::red); + 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); + mLineEdit->setPalette(palette); } } diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index 0d4f2365a6..7af8b8cefa 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -21,6 +21,12 @@ ContentSelectorModel::ContentModel::ContentModel(QObject *parent) : uncheckAll(); } +ContentSelectorModel::ContentModel::~ContentModel() +{ + qDeleteAll(mFiles); + mFiles.clear(); +} + void ContentSelectorModel::ContentModel::setEncoding(const QString &encoding) { mEncoding = encoding; @@ -444,7 +450,9 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) foreach (const QString &path, dir.entryList()) { QFileInfo info(dir.absoluteFilePath(path)); - EsmFile *file = new EsmFile(path); + + if (item(info.absoluteFilePath()) != 0) + continue; try { ESM::ESMReader fileReader; @@ -453,6 +461,8 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) fileReader.setEncoder(&encoder); fileReader.open(dir.absoluteFilePath(path).toStdString()); + EsmFile *file = new EsmFile(path); + foreach (const ESM::Header::MasterData &item, fileReader.getGameFiles()) file->addGameFile(QString::fromStdString(item.name)); @@ -462,10 +472,8 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) file->setFilePath (info.absoluteFilePath()); file->setDescription(decoder->toUnicode(fileReader.getDesc().c_str())); - // Put the file in the table - if (item(file->filePath()) == 0) - addFile(file); + addFile(file); } catch(std::runtime_error &e) { // An error occurred while reading the .esp diff --git a/components/contentselector/model/contentmodel.hpp b/components/contentselector/model/contentmodel.hpp index 7b2000b510..80b5094890 100644 --- a/components/contentselector/model/contentmodel.hpp +++ b/components/contentselector/model/contentmodel.hpp @@ -21,6 +21,7 @@ namespace ContentSelectorModel Q_OBJECT public: explicit ContentModel(QObject *parent = 0); + ~ContentModel(); void setEncoding(const QString &encoding); diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index e9599de498..c8085937cf 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -24,7 +24,7 @@ ContentSelectorView::ContentSelector::ContentSelector(QWidget *parent) : void ContentSelectorView::ContentSelector::buildContentModel() { - mContentModel = new ContentSelectorModel::ContentModel(); + mContentModel = new ContentSelectorModel::ContentModel(this); } void ContentSelectorView::ContentSelector::buildGameFileView() From ad5d88476113e1ac4a4166b6150401c38451e569 Mon Sep 17 00:00:00 2001 From: dteviot Date: Wed, 24 Dec 2014 09:53:49 +1300 Subject: [PATCH 297/303] omwlauncher crash when can't read file in active profile (Fixes #1069) --- .../contentselector/view/contentselector.cpp | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index e9599de498..b12d4147a0 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -64,29 +64,17 @@ void ContentSelectorView::ContentSelector::buildAddonView() void ContentSelectorView::ContentSelector::setProfileContent(const QStringList &fileList) { clearCheckStates(); - bool foundGamefile = false; foreach (const QString &filepath, fileList) { - if (!foundGamefile) + const ContentSelectorModel::EsmFile *file = mContentModel->item(filepath); + if (file && file->isGameFile()) { - const ContentSelectorModel::EsmFile *file = mContentModel->item(filepath); - - foundGamefile = (file->isGameFile()); - - if (foundGamefile) - { - setGameFile (filepath); - break; - } + setGameFile (filepath); + break; } } -/* if (!foundGameFile) - { - //throw gamefile error here. - }*/ - setCheckStates (fileList); } From e85df001587cb0278bc548880c6a07b15f8fac7b Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Wed, 24 Dec 2014 15:09:50 +0100 Subject: [PATCH 298/303] change setting "borderless" to "window border" set window border on setting changes disable window border checkbox in the launcher if fullscreen is enabled --- apps/launcher/graphicspage.cpp | 10 ++++++---- apps/openmw/engine.cpp | 2 +- apps/openmw/mwgui/settingswindow.cpp | 2 +- apps/openmw/mwgui/settingswindow.hpp | 2 +- apps/openmw/mwrender/renderingmanager.cpp | 6 ++++++ files/mygui/openmw_settings_window.layout | 6 +++--- files/settings-default.cfg | 2 +- files/ui/graphicspage.ui | 4 ++-- libs/openengine/ogre/renderer.cpp | 2 +- libs/openengine/ogre/renderer.hpp | 2 +- 10 files changed, 23 insertions(+), 15 deletions(-) diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index 11132f0f47..da707b0056 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -159,8 +159,8 @@ bool Launcher::GraphicsPage::loadSettings() if (mGraphicsSettings.value(QString("Video/fullscreen")) == QLatin1String("true")) fullScreenCheckBox->setCheckState(Qt::Checked); - if (mGraphicsSettings.value(QString("Video/borderless")) == QLatin1String("true")) - borderlessCheckBox->setCheckState(Qt::Checked); + if (mGraphicsSettings.value(QString("Video/window border")) == QLatin1String("true")) + windowBorderCheckBox->setCheckState(Qt::Checked); int aaIndex = antiAliasingComboBox->findText(mGraphicsSettings.value(QString("Video/antialiasing"))); if (aaIndex != -1) @@ -196,8 +196,8 @@ void Launcher::GraphicsPage::saveSettings() fullScreenCheckBox->checkState() ? mGraphicsSettings.setValue(QString("Video/fullscreen"), QString("true")) : mGraphicsSettings.setValue(QString("Video/fullscreen"), QString("false")); - borderlessCheckBox->checkState() ? mGraphicsSettings.setValue(QString("Video/borderless"), QString("true")) - : mGraphicsSettings.setValue(QString("Video/borderless"), QString("false")); + windowBorderCheckBox->checkState() ? mGraphicsSettings.setValue(QString("Video/window border"), QString("true")) + : mGraphicsSettings.setValue(QString("Video/window border"), QString("false")); mGraphicsSettings.setValue(QString("Video/antialiasing"), antiAliasingComboBox->currentText()); mGraphicsSettings.setValue(QString("Video/render system"), rendererComboBox->currentText()); @@ -337,10 +337,12 @@ void Launcher::GraphicsPage::slotFullScreenChanged(int state) 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); } } diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 85127db284..99a642454e 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -350,7 +350,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) OEngine::Render::WindowSettings windowSettings; windowSettings.fullscreen = settings.getBool("fullscreen", "Video"); - windowSettings.borderless = settings.getBool("borderless", "Video"); + windowSettings.window_border = settings.getBool("window border", "Video"); windowSettings.window_x = settings.getInt("resolution x", "Video"); windowSettings.window_y = settings.getInt("resolution y", "Video"); windowSettings.screen = settings.getInt("screen", "Video"); diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 78a46cb6aa..804f023fab 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -166,7 +166,7 @@ namespace MWGui getWidget(mResolutionList, "ResolutionList"); getWidget(mFullscreenButton, "FullscreenButton"); getWidget(mVSyncButton, "VSyncButton"); - getWidget(mBorderlessButton, "BorderlessButton"); + getWidget(mWindowBorderButton, "WindowBorderButton"); getWidget(mFPSButton, "FPSButton"); getWidget(mFOVSlider, "FOVSlider"); getWidget(mAnisotropySlider, "AnisotropySlider"); diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp index 9214fbd6f9..8dcc8dd077 100644 --- a/apps/openmw/mwgui/settingswindow.hpp +++ b/apps/openmw/mwgui/settingswindow.hpp @@ -28,7 +28,7 @@ namespace MWGui MyGUI::ListBox* mResolutionList; MyGUI::Button* mFullscreenButton; MyGUI::Button* mVSyncButton; - MyGUI::Button* mBorderlessButton; + MyGUI::Button* mWindowBorderButton; MyGUI::Button* mFPSButton; MyGUI::ScrollBar* mFOVSlider; MyGUI::ScrollBar* mDifficultySlider; diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index ed25e70a69..2cddbce75c 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -753,6 +753,8 @@ void RenderingManager::processChangedSettings(const Settings::CategorySettingVec || it->second == "resolution y" || it->second == "fullscreen")) changeRes = true; + else if (it->first == "Video" && it->second == "window border") + changeRes = true; else if (it->second == "field of view" && it->first == "General") mRendering.setFov(Settings::Manager::getFloat("field of view", "General")); else if ((it->second == "texture filtering" && it->first == "General") @@ -810,6 +812,7 @@ void RenderingManager::processChangedSettings(const Settings::CategorySettingVec unsigned int x = Settings::Manager::getInt("resolution x", "Video"); unsigned int y = Settings::Manager::getInt("resolution y", "Video"); bool fullscreen = Settings::Manager::getBool("fullscreen", "Video"); + bool windowBorder = Settings::Manager::getBool("window border", "Video"); SDL_Window* window = mRendering.getSDLWindow(); @@ -828,7 +831,10 @@ void RenderingManager::processChangedSettings(const Settings::CategorySettingVec SDL_SetWindowFullscreen(window, fullscreen); } else + { SDL_SetWindowSize(window, x, y); + SDL_SetWindowBordered(window, windowBorder ? SDL_TRUE : SDL_FALSE); + } } mWater->processChangedSettings(settings); diff --git a/files/mygui/openmw_settings_window.layout b/files/mygui/openmw_settings_window.layout index 6e5d8b9e38..e2f46f2d16 100644 --- a/files/mygui/openmw_settings_window.layout +++ b/files/mygui/openmw_settings_window.layout @@ -240,13 +240,13 @@ - + - + - + diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 69a9b2db5c..df8266f7af 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -6,7 +6,7 @@ resolution x = 800 resolution y = 600 fullscreen = false -borderless = false +window border = true screen = 0 # Minimize the window if it loses key focus? diff --git a/files/ui/graphicspage.ui b/files/ui/graphicspage.ui index 753d65d033..f9ea63efe6 100644 --- a/files/ui/graphicspage.ui +++ b/files/ui/graphicspage.ui @@ -51,9 +51,9 @@ - + - Borderless + Window border diff --git a/libs/openengine/ogre/renderer.cpp b/libs/openengine/ogre/renderer.cpp index fadfc11a64..404602c304 100644 --- a/libs/openengine/ogre/renderer.cpp +++ b/libs/openengine/ogre/renderer.cpp @@ -130,7 +130,7 @@ void OgreRenderer::createWindow(const std::string &title, const WindowSettings& SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | (settings.fullscreen ? SDL_WINDOW_FULLSCREEN : 0) - | (settings.borderless ? SDL_WINDOW_BORDERLESS : 0) + | (settings.window_border ? 0 : SDL_WINDOW_BORDERLESS) ); SFO::SDLWindowHelper helper(mSDLWindow, settings.window_x, settings.window_y, title, settings.fullscreen, params); diff --git a/libs/openengine/ogre/renderer.hpp b/libs/openengine/ogre/renderer.hpp index 5f1f6a4bb2..70cc3db60f 100644 --- a/libs/openengine/ogre/renderer.hpp +++ b/libs/openengine/ogre/renderer.hpp @@ -37,7 +37,7 @@ namespace OEngine { bool vsync; bool fullscreen; - bool borderless; + bool window_border; int window_x, window_y; int screen; std::string fsaa; From 764cd9ca163746ac5aa4b00a3125b4efe73f6070 Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Wed, 24 Dec 2014 16:31:23 +0100 Subject: [PATCH 299/303] disable "window border" setting in the ingame settings UI if fullscreen is enabled --- apps/openmw/mwgui/settingswindow.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 804f023fab..ce2a20d8ba 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -240,6 +240,8 @@ namespace MWGui MyGUI::TextBox* diffText; getWidget(diffText, "DifficultyText"); diffText->setCaptionWithReplacing("#{sDifficulty} (" + boost::lexical_cast(int(Settings::Manager::getInt("difficulty", "Game"))) + ")"); + + mWindowBorderButton->setEnabled(!Settings::Manager::getBool("fullscreen", "Video")); } void SettingsWindow::onOkButtonClicked(MyGUI::Widget* _sender) @@ -355,6 +357,8 @@ namespace MWGui _sender->castType()->setCaption(off); return; } + + mWindowBorderButton->setEnabled(!newState); } if (getSettingType(_sender) == checkButtonType) From 2fa39c45816a7f4dff9834bde170898472991a44 Mon Sep 17 00:00:00 2001 From: Nikolay Kasyanov Date: Thu, 25 Dec 2014 14:22:27 +0300 Subject: [PATCH 300/303] Do not install formulae that are already present on Travis instances, let's check if that would make Travis happy --- CI/before_install.osx.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh index b1d4f991ba..165763efa6 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -6,4 +6,4 @@ export CC=clang brew tap openmw/openmw brew update brew unlink boost -brew install cmake openmw-mygui openmw-bullet openmw-sdl2 openmw-ffmpeg pkg-config qt unshield +brew install openmw-mygui openmw-bullet openmw-sdl2 openmw-ffmpeg qt unshield From 49e2b14d0554c8cb49c5bc1061231bc33fd1da07 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Fri, 26 Dec 2014 16:30:20 +0100 Subject: [PATCH 301/303] updated credits file --- credits.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/credits.txt b/credits.txt index c3eab721fe..ebb2f8f26b 100644 --- a/credits.txt +++ b/credits.txt @@ -31,6 +31,7 @@ Dmitry Shkurskiy (endorph) Douglas Diniz (Dgdiniz) Douglas Mencken (dougmencken) dreamer-dead +dteviot Edmondo Tommasina (edmondo) Eduard Cot (trombonecot) Eli2 From d92cd2ffad226a9b3d140d546ef549c1b52bb022 Mon Sep 17 00:00:00 2001 From: Marco Schulze Date: Sat, 27 Dec 2014 00:26:35 -0300 Subject: [PATCH 302/303] Remove GetGitRevisionDescription.cmake GetGitRevisionDescription.cmake uses a somewhat contrived method to obtain the hash of the commit pointed by the repository's HEAD. This method fails on unusual, but still valid repository layouts. This commit removes cmake/GetGitRevisionDescription.cmake{,.in}, replacing its functionality with direct use of Git's plumbing. --- CMakeLists.txt | 28 +++-- cmake/GetGitRevisionDescription.cmake | 154 ----------------------- cmake/GetGitRevisionDescription.cmake.in | 38 ------ 3 files changed, 16 insertions(+), 204 deletions(-) delete mode 100644 cmake/GetGitRevisionDescription.cmake delete mode 100644 cmake/GetGitRevisionDescription.cmake.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 9587c652c6..d7c85818e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,24 +25,28 @@ if(EXISTS ${PROJECT_SOURCE_DIR}/.git) find_package(Git) if(GIT_FOUND) - include(GetGitRevisionDescription) - get_git_tag_revision(TAGHASH --tags --max-count=1) - get_git_head_revision(REFSPEC COMMITHASH) - git_describe(VERSION --tags ${TAGHASH}) + execute_process ( + COMMAND "${GIT_EXECUTABLE}" rev-list --tags --max-count=1 + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + RESULT_VARIABLE EXITCODE1 + OUTPUT_VARIABLE TAGHASH + OUTPUT_STRIP_TRAILING_WHITESPACE) - string(REGEX MATCH "^openmw-[^0-9]*[0-9]+\\.[0-9]+\\.[0-9]+.*" MATCH "${VERSION}") - if(MATCH) - string(REGEX REPLACE "^openmw-([0-9]+)\\..*" "\\1" GIT_VERSION_MAJOR "${VERSION}") - string(REGEX REPLACE "^openmw-[0-9]+\\.([0-9]+).*" "\\1" GIT_VERSION_MINOR "${VERSION}") - string(REGEX REPLACE "^openmw-[0-9]+\\.[0-9]+\\.([0-9]+).*" "\\1" GIT_VERSION_RELEASE "${VERSION}") + execute_process ( + COMMAND "${GIT_EXECUTABLE}" rev-parse HEAD + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + RESULT_VARIABLE EXITCODE2 + OUTPUT_VARIABLE COMMITHASH + OUTPUT_STRIP_TRAILING_WHITESPACE) + string (COMPARE EQUAL "${EXITCODE1}:${EXITCODE2}" "0:0" SUCCESS) + if (SUCCESS) set(OPENMW_VERSION_COMMITHASH "${COMMITHASH}") set(OPENMW_VERSION_TAGHASH "${TAGHASH}") - message(STATUS "OpenMW version ${OPENMW_VERSION}") - else(MATCH) + else (SUCCESS) message(WARNING "Failed to get valid version information from Git") - endif(MATCH) + endif (SUCCESS) else(GIT_FOUND) message(WARNING "Git executable not found") endif(GIT_FOUND) diff --git a/cmake/GetGitRevisionDescription.cmake b/cmake/GetGitRevisionDescription.cmake deleted file mode 100644 index 56ff1d5459..0000000000 --- a/cmake/GetGitRevisionDescription.cmake +++ /dev/null @@ -1,154 +0,0 @@ -# - Returns a version string from Git -# -# These functions force a re-configure on each git commit so that you can -# trust the values of the variables in your build system. -# -# get_git_head_revision( [ ...]) -# -# Returns the refspec and sha hash of the current head revision -# -# git_describe( [ ...]) -# -# Returns the results of git describe on the source tree, and adjusting -# the output so that it tests false if an error occurs. -# -# git_get_exact_tag( [ ...]) -# -# Returns the results of git describe --exact-match on the source tree, -# and adjusting the output so that it tests false if there was no exact -# matching tag. -# -# Requires CMake 2.6 or newer (uses the 'function' command) -# -# Original Author: -# 2009-2010 Ryan Pavlik -# http://academic.cleardefinition.com -# Iowa State University HCI Graduate Program/VRAC -# -# Copyright Iowa State University 2009-2010. -# Distributed under the Boost Software License, Version 1.0. -# (See accompanying file LICENSE_1_0.txt or copy at -# http://www.boost.org/LICENSE_1_0.txt) - -if(__get_git_revision_description) - return() -endif() -set(__get_git_revision_description YES) - -# We must run the following at "include" time, not at function call time, -# to find the path to this module rather than the path to a calling list file -get_filename_component(_gitdescmoddir ${CMAKE_CURRENT_LIST_FILE} PATH) - -function(get_git_head_revision _refspecvar _hashvar) - set(GIT_PARENT_DIR "${CMAKE_CURRENT_SOURCE_DIR}") - set(GIT_DIR "${GIT_PARENT_DIR}/.git") - while(NOT EXISTS "${GIT_DIR}") # .git dir not found, search parent directories - set(GIT_PREVIOUS_PARENT "${GIT_PARENT_DIR}") - get_filename_component(GIT_PARENT_DIR ${GIT_PARENT_DIR} PATH) - if(GIT_PARENT_DIR STREQUAL GIT_PREVIOUS_PARENT) - # We have reached the root directory, we are not in git - set(${_refspecvar} "GITDIR-NOTFOUND" PARENT_SCOPE) - set(${_hashvar} "GITDIR-NOTFOUND" PARENT_SCOPE) - return() - endif() - - set(GIT_DIR "${GIT_PARENT_DIR}/.git") - endwhile() - - # check if this is a submodule - if(NOT IS_DIRECTORY ${GIT_DIR}) - file(READ ${GIT_DIR} submodule) - string(REGEX REPLACE "gitdir: (.*)\n$" "\\1" GIT_DIR_RELATIVE ${submodule}) - get_filename_component(SUBMODULE_DIR ${GIT_DIR} PATH) - get_filename_component(GIT_DIR ${SUBMODULE_DIR}/${GIT_DIR_RELATIVE} ABSOLUTE) - endif() - - set(GIT_DATA "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/git-data") - - if(NOT EXISTS "${GIT_DATA}") - file(MAKE_DIRECTORY "${GIT_DATA}") - endif() - - if(NOT EXISTS "${GIT_DIR}/HEAD") - return() - endif() - - set(HEAD_FILE "${GIT_DATA}/HEAD") - configure_file("${GIT_DIR}/HEAD" "${HEAD_FILE}" COPYONLY) - - configure_file("${_gitdescmoddir}/GetGitRevisionDescription.cmake.in" - "${GIT_DATA}/grabRef.cmake" @ONLY) - include("${GIT_DATA}/grabRef.cmake") - - set(${_refspecvar} "${HEAD_REF}" PARENT_SCOPE) - set(${_hashvar} "${HEAD_HASH}" PARENT_SCOPE) -endfunction() - -function(git_describe _var) - #get_git_head_revision(refspec hash) - - if(NOT GIT_FOUND) - set(${_var} "GIT-NOTFOUND" PARENT_SCOPE) - return() - endif() - - #if(NOT hash) - # set(${_var} "HEAD-HASH-NOTFOUND" PARENT_SCOPE) - # return() - #endif() - - # TODO sanitize - #if((${ARGN}" MATCHES "&&") OR - # (ARGN MATCHES "||") OR - # (ARGN MATCHES "\\;")) - # message("Please report the following error to the project!") - # message(FATAL_ERROR "Looks like someone's doing something nefarious with git_describe! Passed arguments ${ARGN}") - #endif() - - #message(STATUS "Arguments to execute_process: ${ARGN}") - - execute_process(COMMAND - "${GIT_EXECUTABLE}" - describe - #${hash} - ${ARGN} - WORKING_DIRECTORY - "${CMAKE_SOURCE_DIR}" - RESULT_VARIABLE - res - OUTPUT_VARIABLE - out - OUTPUT_STRIP_TRAILING_WHITESPACE) - - if(NOT res EQUAL 0) - set(out "${out}-${res}-NOTFOUND") - endif() - - set(${_var} "${out}" PARENT_SCOPE) -endfunction() - -function(get_git_tag_revision _var) - if(NOT GIT_FOUND) - set(${_var} "GIT-NOTFOUND" PARENT_SCOPE) - return() - endif() - - execute_process(COMMAND - "${GIT_EXECUTABLE}" - rev-list - ${ARGN} - WORKING_DIRECTORY - "${CMAKE_SOURCE_DIR}" - RESULT_VARIABLE - res - OUTPUT_VARIABLE - out - OUTPUT_STRIP_TRAILING_WHITESPACE) - if(NOT res EQUAL 0) - set(out "${out}-${res}-NOTFOUND") - endif() - - set(${_var} "${out}" PARENT_SCOPE) -endfunction() - - diff --git a/cmake/GetGitRevisionDescription.cmake.in b/cmake/GetGitRevisionDescription.cmake.in deleted file mode 100644 index 888ce13aab..0000000000 --- a/cmake/GetGitRevisionDescription.cmake.in +++ /dev/null @@ -1,38 +0,0 @@ -# -# Internal file for GetGitRevisionDescription.cmake -# -# Requires CMake 2.6 or newer (uses the 'function' command) -# -# Original Author: -# 2009-2010 Ryan Pavlik -# http://academic.cleardefinition.com -# Iowa State University HCI Graduate Program/VRAC -# -# Copyright Iowa State University 2009-2010. -# Distributed under the Boost Software License, Version 1.0. -# (See accompanying file LICENSE_1_0.txt or copy at -# http://www.boost.org/LICENSE_1_0.txt) - -set(HEAD_HASH) - -file(READ "@HEAD_FILE@" HEAD_CONTENTS LIMIT 1024) - -string(STRIP "${HEAD_CONTENTS}" HEAD_CONTENTS) -if(HEAD_CONTENTS MATCHES "ref") - # named branch - string(REPLACE "ref: " "" HEAD_REF "${HEAD_CONTENTS}") - if(EXISTS "@GIT_DIR@/${HEAD_REF}") - configure_file("@GIT_DIR@/${HEAD_REF}" "@GIT_DATA@/head-ref" COPYONLY) - elseif(EXISTS "@GIT_DIR@/logs/${HEAD_REF}") - configure_file("@GIT_DIR@/logs/${HEAD_REF}" "@GIT_DATA@/head-ref" COPYONLY) - set(HEAD_HASH "${HEAD_REF}") - endif() -else() - # detached HEAD - configure_file("@GIT_DIR@/HEAD" "@GIT_DATA@/head-ref" COPYONLY) -endif() - -if(NOT HEAD_HASH) - file(READ "@GIT_DATA@/head-ref" HEAD_HASH LIMIT 1024) - string(STRIP "${HEAD_HASH}" HEAD_HASH) -endif() From 82ef145e795bbc7b8ad302d11d8cee7149e13fcb Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 27 Dec 2014 13:13:01 +0100 Subject: [PATCH 303/303] updated credits file --- credits.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/credits.txt b/credits.txt index ebb2f8f26b..dfdf875653 100644 --- a/credits.txt +++ b/credits.txt @@ -64,6 +64,7 @@ Marc Bouvier (CramitDeFrog) Marcin Hulist (Gohan) Mark Siewert (mark76) Marco Melletti (mellotanica) +Marco Schulze Mateusz Kołaczek (PL_kolek) megaton Michael Hogan (Xethik)