From 841ea105746b5d6eef41f5bae02e5ec4b3342547 Mon Sep 17 00:00:00 2001 From: Koncord Date: Thu, 30 Aug 2018 05:53:18 +0800 Subject: [PATCH] Initial commit --- .gitmodules | 3 + CMakeLists.txt | 197 +++++++ LICENSE | 674 ++++++++++++++++++++++ README.md | 22 + apps/browser/CMakeLists.txt | 103 ++++ apps/browser/Data.hpp | 98 ++++ apps/browser/MainWindow.cpp | 367 ++++++++++++ apps/browser/MainWindow.hpp | 53 ++ apps/browser/MySortFilterProxyModel.cpp | 84 +++ apps/browser/MySortFilterProxyModel.hpp | 24 + apps/browser/PingHelper.cpp | 56 ++ apps/browser/PingHelper.hpp | 38 ++ apps/browser/PingUpdater.cpp | 50 ++ apps/browser/PingUpdater.hpp | 29 + apps/browser/QueryHelper.cpp | 73 +++ apps/browser/QueryHelper.hpp | 28 + apps/browser/ServerInfoDialog.cpp | 140 +++++ apps/browser/ServerInfoDialog.hpp | 39 ++ apps/browser/ServerModel.cpp | 210 +++++++ apps/browser/ServerModel.hpp | 30 + apps/browser/Settings.cpp | 196 +++++++ apps/browser/Settings.hpp | 34 ++ apps/browser/SettingsWindow.cpp | 29 + apps/browser/SettingsWindow.hpp | 20 + apps/browser/Types.hpp | 11 + apps/browser/Version.hpp | 16 + apps/browser/main.cpp | 21 + apps/browser/netutils/MasterClient.cpp | 270 +++++++++ apps/browser/netutils/MasterClient.hpp | 77 +++ apps/browser/netutils/Utils.cpp | 61 ++ apps/browser/netutils/Utils.hpp | 13 + cmake/FindRakNet.cmake | 75 +++ components/CMakeLists.txt | 36 ++ components/files/collections.cpp | 85 +++ components/files/collections.hpp | 43 ++ components/files/configurationmanager.cpp | 202 +++++++ components/files/configurationmanager.hpp | 67 +++ components/files/escape.cpp | 145 +++++ components/files/escape.hpp | 191 ++++++ components/files/fixedpath.hpp | 129 +++++ components/files/linuxpath.cpp | 160 +++++ components/files/linuxpath.hpp | 61 ++ components/files/multidircollection.cpp | 107 ++++ components/files/multidircollection.hpp | 88 +++ components/misc/stringops.hpp | 241 ++++++++ components/misc/utf8stream.hpp | 122 ++++ components/process/processinvoker.cpp | 185 ++++++ components/process/processinvoker.hpp | 43 ++ components/settings/settings.cpp | 443 ++++++++++++++ components/settings/settings.hpp | 54 ++ extern/json | 1 + files/tes3mp-browser.desktop | 10 + files/tes3mp/browser.qrc | 5 + files/tes3mp/browser.rc | 1 + files/tes3mp/tes3mp-browser-default.cfg | 13 + files/tes3mp/tes3mp.ico | Bin 0 -> 5430 bytes files/tes3mp/tes3mp_logo.png | Bin 0 -> 107752 bytes files/tes3mp/ui/Login.ui | 99 ++++ files/tes3mp/ui/Main.ui | 287 +++++++++ files/tes3mp/ui/ServerInfo.ui | 297 ++++++++++ files/tes3mp/ui/Settings.ui | 625 ++++++++++++++++++++ 61 files changed, 6881 insertions(+) create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 apps/browser/CMakeLists.txt create mode 100644 apps/browser/Data.hpp create mode 100644 apps/browser/MainWindow.cpp create mode 100644 apps/browser/MainWindow.hpp create mode 100644 apps/browser/MySortFilterProxyModel.cpp create mode 100644 apps/browser/MySortFilterProxyModel.hpp create mode 100644 apps/browser/PingHelper.cpp create mode 100644 apps/browser/PingHelper.hpp create mode 100644 apps/browser/PingUpdater.cpp create mode 100644 apps/browser/PingUpdater.hpp create mode 100644 apps/browser/QueryHelper.cpp create mode 100644 apps/browser/QueryHelper.hpp create mode 100644 apps/browser/ServerInfoDialog.cpp create mode 100644 apps/browser/ServerInfoDialog.hpp create mode 100644 apps/browser/ServerModel.cpp create mode 100644 apps/browser/ServerModel.hpp create mode 100644 apps/browser/Settings.cpp create mode 100644 apps/browser/Settings.hpp create mode 100644 apps/browser/SettingsWindow.cpp create mode 100644 apps/browser/SettingsWindow.hpp create mode 100644 apps/browser/Types.hpp create mode 100644 apps/browser/Version.hpp create mode 100644 apps/browser/main.cpp create mode 100644 apps/browser/netutils/MasterClient.cpp create mode 100644 apps/browser/netutils/MasterClient.hpp create mode 100644 apps/browser/netutils/Utils.cpp create mode 100644 apps/browser/netutils/Utils.hpp create mode 100644 cmake/FindRakNet.cmake create mode 100644 components/CMakeLists.txt create mode 100644 components/files/collections.cpp create mode 100644 components/files/collections.hpp create mode 100644 components/files/configurationmanager.cpp create mode 100644 components/files/configurationmanager.hpp create mode 100644 components/files/escape.cpp create mode 100644 components/files/escape.hpp create mode 100644 components/files/fixedpath.hpp create mode 100644 components/files/linuxpath.cpp create mode 100644 components/files/linuxpath.hpp create mode 100644 components/files/multidircollection.cpp create mode 100644 components/files/multidircollection.hpp create mode 100644 components/misc/stringops.hpp create mode 100644 components/misc/utf8stream.hpp create mode 100644 components/process/processinvoker.cpp create mode 100644 components/process/processinvoker.hpp create mode 100644 components/settings/settings.cpp create mode 100644 components/settings/settings.hpp create mode 160000 extern/json create mode 100644 files/tes3mp-browser.desktop create mode 100644 files/tes3mp/browser.qrc create mode 100644 files/tes3mp/browser.rc create mode 100644 files/tes3mp/tes3mp-browser-default.cfg create mode 100644 files/tes3mp/tes3mp.ico create mode 100644 files/tes3mp/tes3mp_logo.png create mode 100644 files/tes3mp/ui/Login.ui create mode 100644 files/tes3mp/ui/Main.ui create mode 100644 files/tes3mp/ui/ServerInfo.ui create mode 100644 files/tes3mp/ui/Settings.ui diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f8efac8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "extern/json"] + path = extern/json + url = https://github.com/nlohmann/json diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..648bd09 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,197 @@ +# set the minimum required version across the board +cmake_minimum_required(VERSION 3.5.0) + +project(tes3mp-Browser) + +# If the user doesn't supply a CMAKE_BUILD_TYPE via command line, choose one for them. +IF(NOT CMAKE_BUILD_TYPE) + SET(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING + "Choose the type of build, options are: None(CMAKE_CXX_FLAGS or CMAKE_C_FLAGS used) Debug Release RelWithDebInfo MinSizeRel." + FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS None Debug Release RelWithDebInfo MinSizeRel) +ENDIF() + +set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/) + +option(BOOST_STATIC "Link static build of Boost into the binaries" FALSE) +option(QT_STATIC "Link static build of QT into the binaries" FALSE) + +if (MSVC) + option(OPENMW_MP_BUILD "Build OpenMW with /MP flag" OFF) + option(OPENMW_LTO_BUILD "Build OpenMW with Link-Time Optimization (Needs ~2GB of RAM)" OFF) +endif() + +# Set up common paths +if (APPLE) + set(MORROWIND_DATA_FILES "./data" CACHE PATH "location of Morrowind data files") + set(OPENMW_RESOURCE_FILES "../Resources/resources" CACHE PATH "location of OpenMW resources files") +elseif(UNIX) + # Paths + SET(BINDIR "${CMAKE_INSTALL_PREFIX}/bin" CACHE PATH "Where to install binaries") + SET(LIBDIR "${CMAKE_INSTALL_PREFIX}/lib${LIB_SUFFIX}" CACHE PATH "Where to install libraries") + SET(DATAROOTDIR "${CMAKE_INSTALL_PREFIX}/share" CACHE PATH "Sets the root of data directories to a non-default location") + SET(GLOBAL_DATA_PATH "${DATAROOTDIR}/games/" CACHE PATH "Set data path prefix") + SET(DATADIR "${GLOBAL_DATA_PATH}/openmw" CACHE PATH "Sets the openmw data directories to a non-default location") + SET(ICONDIR "${DATAROOTDIR}/pixmaps" CACHE PATH "Set icon dir") + SET(LICDIR "${DATAROOTDIR}/licenses/openmw" CACHE PATH "Sets the openmw license directory to a non-default location.") + IF("${CMAKE_INSTALL_PREFIX}" STREQUAL "/usr") + SET(GLOBAL_CONFIG_PATH "/etc/" CACHE PATH "Set config dir prefix") + ELSE() + SET(GLOBAL_CONFIG_PATH "${CMAKE_INSTALL_PREFIX}/etc/" CACHE PATH "Set config dir prefix") + ENDIF() + SET(SYSCONFDIR "${GLOBAL_CONFIG_PATH}/openmw" CACHE PATH "Set config dir") + + set(MORROWIND_DATA_FILES "${DATADIR}/data" CACHE PATH "location of Morrowind data files") + set(OPENMW_RESOURCE_FILES "${DATADIR}/resources" CACHE PATH "location of OpenMW resources files") +else() + set(MORROWIND_DATA_FILES "data" CACHE PATH "location of Morrowind data files") + set(OPENMW_RESOURCE_FILES "resources" CACHE PATH "location of OpenMW resources files") +endif(APPLE) + + +find_package(Qt5Widgets REQUIRED) +find_package(Qt5Core REQUIRED) +find_package(Qt5Network REQUIRED) +find_package(Qt5WebSockets REQUIRED) + +# Instruct CMake to run moc automatically when needed. +#set(CMAKE_AUTOMOC ON) + + +# Platform specific +if (WIN32) + if(NOT MINGW) + set(Boost_USE_STATIC_LIBS ON) + add_definitions(-DBOOST_ALL_NO_LIB) + endif(NOT MINGW) + + # Suppress WinMain(), provided by SDL + add_definitions(-DSDL_MAIN_HANDLED) + + # Get rid of useless crud from windows.h + add_definitions(-DNOMINMAX -DWIN32_LEAN_AND_MEAN) +endif() + +# Fix for not visible pthreads functions for linker with glibc 2.15 +if (UNIX AND NOT APPLE) + find_package (Threads) +endif() + +# Look for stdint.h +include(CheckIncludeFile) +check_include_file(stdint.h HAVE_STDINT_H) +if(NOT HAVE_STDINT_H) + unset(HAVE_STDINT_H CACHE) + message(FATAL_ERROR "stdint.h was not found" ) +endif() + +set(BOOST_COMPONENTS system filesystem program_options) +if(WIN32) + set(BOOST_COMPONENTS ${BOOST_COMPONENTS} locale) +endif(WIN32) + +IF(BOOST_STATIC) + set(Boost_USE_STATIC_LIBS ON) +endif() + +find_package(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS}) +find_package(RakNet REQUIRED) + +set(NJSON_INCLUDES "${CMAKE_SOURCE_DIR}/extern/json/single_include/nlohmann") + +include_directories("." + SYSTEM + ${Boost_INCLUDE_DIR} + ${RakNet_INCLUDES} + ${NJSON_INCLUDES} +) + +link_directories(${Boost_LIBRARY_DIRS}) + +if (APPLE) + configure_file(${OpenMW_SOURCE_DIR}/files/mac/openmw-Info.plist.in + "${APP_BUNDLE_DIR}/Contents/Info.plist") + + configure_file(${OpenMW_SOURCE_DIR}/files/mac/openmw.icns + "${APP_BUNDLE_DIR}/Contents/Resources/OpenMW.icns" COPYONLY) +endif (APPLE) + +# Set up DEBUG define +set_directory_properties(PROPERTIES COMPILE_DEFINITIONS_DEBUG DEBUG=1) + +if (NOT APPLE) + set(OPENMW_MYGUI_FILES_ROOT ${OpenMW_BINARY_DIR}) + set(OPENMW_SHADERS_ROOT ${OpenMW_BINARY_DIR}) +endif () + +# Specify build paths + +if (APPLE) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${APP_BUNDLE_DIR}/Contents/MacOS") + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${APP_BUNDLE_DIR}/Contents/MacOS") + + if (OPENMW_OSX_DEPLOYMENT) + SET(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) + endif() +else (APPLE) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${OpenMW_BINARY_DIR}") + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${OpenMW_BINARY_DIR}") +endif (APPLE) + +# CXX Compiler settings +set(CMAKE_CXX_STANDARD 17) + +if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wundef -Wno-unused-parameter -pedantic -Wno-long-long") + add_definitions( -DBOOST_NO_CXX11_SCOPED_ENUMS=ON ) + + if (APPLE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -stdlib=libc++") + endif() + + if (CMAKE_CXX_COMPILER_ID STREQUAL Clang AND NOT APPLE) + if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 3.6 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 3.6) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-potentially-evaluated-expression") + endif () + endif() + + if (CMAKE_CXX_COMPILER_ID STREQUAL GNU AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.6 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 4.6) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-but-set-parameter") + endif() +elseif (MSVC) + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS} /Zi /bigobj") + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:REF /OPT:ICF /INCREMENTAL:NO") + # Enable link-time code generation globally for all linking + if (OPENMW_LTO_BUILD) + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /GL") + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /LTCG") + set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /LTCG") + set(CMAKE_STATIC_LINKER_FLAGS_RELEASE "${CMAKE_STATIC_LINKER_FLAGS_RELEASE} /LTCG") + endif() + + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /FORCE:MULTIPLE") +endif (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) + +IF(NOT WIN32 AND NOT APPLE) + # Linux installation + + # Install binaries + + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/tes3mp-browser" DESTINATION "${BINDIR}" ) + + + # Install licenses + INSTALL(FILES "files/mygui/DejaVu Font License.txt" DESTINATION "${LICDIR}" ) + + # Install icon and desktop file + INSTALL(FILES "${OpenMW_BINARY_DIR}/tes3mp-browser.desktop" DESTINATION "${DATAROOTDIR}/applications" COMPONENT "browser") + + +ENDIF(NOT WIN32 AND NOT APPLE) + +# Components +add_subdirectory (components) + +# Apps and tools +add_subdirectory(apps/browser) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9d74247 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ec4807 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +TES3MP Server Browser +===================== +This is the 2nd generation of the server browser, using the 3rd implementation of the Master Server protocol. +The browser's code base has been separated from the main TES3MP repo, which means it is now independent from TES3MP versions. + +The Master Server Protocol v3 is licensed under the MIT License, but the master server currently does not have an open source implementation. + +Requirements +------------ +* C++ compiler with C++17 support +* CMake 3.5 or higher +* QT5 (Core, Widgets, Network, WebSockets) +* Boost (System, File System, Program Options) +* CrabNet (https://github.com/TES3MP/CrabNet) + +``Attention! Do not forget to clone git submodules!`` + +Licenses and Copyrights +----------------------- +This project is licensed under the GPLv3 license (see LICENSE). + +All the files hosted in the components directory are copyrighted by the OpenMW project and licensed under the GPLv3. diff --git a/apps/browser/CMakeLists.txt b/apps/browser/CMakeLists.txt new file mode 100644 index 0000000..88f1c0a --- /dev/null +++ b/apps/browser/CMakeLists.txt @@ -0,0 +1,103 @@ +option(OPTION_TES3MP_PRE07 "Temporary. Pre 0.7.0 compatible mode." OFF) + +set(BROWSER_UI + ${CMAKE_SOURCE_DIR}/files/tes3mp/ui/Main.ui + ${CMAKE_SOURCE_DIR}/files/tes3mp/ui/ServerInfo.ui + ${CMAKE_SOURCE_DIR}/files/tes3mp/ui/Settings.ui + ${CMAKE_SOURCE_DIR}/files/tes3mp/ui/Login.ui + ) +set(BROWSER + main.cpp + MainWindow.cpp + ServerModel.cpp + ServerInfoDialog.cpp + MySortFilterProxyModel.cpp + netutils/Utils.cpp + PingUpdater.cpp + PingHelper.cpp + QueryHelper.cpp + Settings.cpp + SettingsWindow.cpp + netutils/MasterClient.cpp + ${CMAKE_SOURCE_DIR}/files/tes3mp/browser.rc + ) + +set(BROWSER_HEADER_MOC + MainWindow.hpp + ServerModel.hpp + ServerInfoDialog.hpp + MySortFilterProxyModel.hpp + PingUpdater.hpp + PingHelper.hpp + QueryHelper.hpp + SettingsWindow.hpp + netutils/MasterClient.hpp + ) + +set(BROWSER_HEADER + ${BROWSER_HEADER_MOC} + netutils/Utils.hpp + Types.hpp + Settings.hpp + Data.hpp + ) + +source_group(browser FILES ${BROWSER} ${BROWSER_HEADER}) + +set(QT_USE_QTGUI 1) + +# Set some platform specific settings +if(WIN32) + set(GUI_TYPE WIN32) + set(QT_USE_QTMAIN TRUE) +endif(WIN32) + + +QT5_ADD_RESOURCES(RCC_SRCS ${CMAKE_SOURCE_DIR}/files/tes3mp/browser.qrc) +QT5_WRAP_CPP(MOC_SRCS ${BROWSER_HEADER_MOC}) +QT5_WRAP_UI(UI_HDRS ${BROWSER_UI}) + + +include_directories(${CMAKE_CURRENT_BINARY_DIR}) +if(NOT WIN32) + include_directories(${LIBUNSHIELD_INCLUDE_DIR}) +endif(NOT WIN32) + +# Main executable +add_executable(tes3mp-browser + ${GUI_TYPE} + ${BROWSER} + ${BROWSER_HEADER} + ${RCC_SRCS} + ${MOC_SRCS} + ${UI_HDRS} + ) + +if (OPTION_TES3MP_PRE07) + target_compile_definitions(tes3mp-browser PRIVATE TES3MP_PRE07) +endif (OPTION_TES3MP_PRE07) + + +if (WIN32) + INSTALL(TARGETS tes3mp-browser RUNTIME DESTINATION ".") +endif (WIN32) + +target_link_libraries(tes3mp-browser + ${SDL2_LIBRARY_ONLY} + ${RakNet_LIBRARY} + components + ) + +if (DESIRED_QT_VERSION MATCHES 4) +# target_link_libraries(tes3mp-browser ${QT_QTGUI_LIBRARY} ${QT_QTCORE_LIBRARY}) +# if(WIN32) +# target_link_libraries(tes3mp-browser ${QT_QTMAIN_LIBRARY}) +# endif(WIN32) +else() + qt5_use_modules(tes3mp-browser Widgets Core Network WebSockets) +endif() + +if (BUILD_WITH_CODE_COVERAGE) + add_definitions (--coverage) + target_link_libraries(tes3mp-browser gcov) +endif() diff --git a/apps/browser/Data.hpp b/apps/browser/Data.hpp new file mode 100644 index 0000000..7fe1abe --- /dev/null +++ b/apps/browser/Data.hpp @@ -0,0 +1,98 @@ +// +// Created by koncord on 01.07.18. +// + +#pragma once + +#include +#include +#include +#include +#include +#include "Version.hpp" + + +struct Server +{ + std::string id; + std::string address; + unsigned short port; + std::string modname, hostname, version; + int players, maxPlayers; + int ping; + bool password; + enum IDS + { + ID, + ADDR, + PORT, + HOSTNAME, + PLAYERS, + MAX_PLAYERS, + PASSW, + MODNAME, + PING, + VERSION, + LAST + }; +}; + +struct ServerExtra +{ + std::string dlServer; + std::vector players; + std::vector plugins; + std::unordered_map extraInfo; +}; + +Q_DECLARE_METATYPE(Server) +Q_DECLARE_METATYPE(ServerExtra) + +/*struct BasicInfo +{ + QString addr; + unsigned short port; + QString hostname; + unsigned int players, maxPlayers; + bool hasPassword; + QString modname; + int ping; + QString version; + enum IDS + { + ADDR, + HOSTNAME, + PLAYERS, + MAX_PLAYERS, + PASSW, + MODNAME, + PING, + VERSION, + LAST + }; +}; + +struct FullInfo: public BasicInfo +{ + enum class ExtraType: char + { + String, + Number + }; + + struct ExtraInfo + { + QString key; + ExtraType type; + QVariant value; + }; + + struct Plugin + { + QString plugin, hash; + }; + + QStringList players; + QVector pluginHashes; + QVector extraInfo; +};*/ diff --git a/apps/browser/MainWindow.cpp b/apps/browser/MainWindow.cpp new file mode 100644 index 0000000..77fac87 --- /dev/null +++ b/apps/browser/MainWindow.cpp @@ -0,0 +1,367 @@ +// +// Created by koncord on 06.01.17. +// + +#include "MainWindow.hpp" +#include "QueryHelper.hpp" +#include "PingHelper.hpp" +#include "ServerInfoDialog.hpp" +#include "SettingsWindow.hpp" +#include "Settings.hpp" + +#include "components/files/configurationmanager.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "netutils/Utils.hpp" +#include "netutils/MasterClient.hpp" + +using namespace Process; +using namespace std; + +class ImageButton final: public QAbstractButton +{ +public: + ImageButton(QWidget *parent) : QAbstractButton(parent) { setCursor(Qt::PointingHandCursor); } + + void setPixmap(const QPixmap &pm) + { + pixmap = pm; + update(); + } + + QSize sizeHint() const override { return pixmap.size(); } + +protected: + void paintEvent(QPaintEvent *e) override + { + QPainter painter(this); + painter.drawPixmap(0, 0, pixmap); + } +private: + QPixmap pixmap; +}; + +MainWindow::MainWindow(QWidget *parent) +{ + setupUi(this); + + MasterClient::create(this); + + mGameInvoker = new ProcessInvoker(); + + browser = new ServerModel; + favorites = new ServerModel; + proxyModel = new MySortFilterProxyModel(this); + proxyModel->setSourceModel(browser); + tblServerBrowser->setModel(proxyModel); + tblFavorites->setModel(proxyModel); + + tblServerBrowser->hideColumn(Server::ADDR); + tblFavorites->hideColumn(Server::ADDR); + tblServerBrowser->hideColumn(Server::PORT); + tblFavorites->hideColumn(Server::PORT); + tblServerBrowser->hideColumn(Server::ID); + tblFavorites->hideColumn(Server::ID); + + PingHelper::Get().SetModel((ServerModel *) proxyModel->sourceModel()); + queryHelper = new QueryHelper(proxyModel->sourceModel()); + connect(queryHelper, &QueryHelper::started, [this]() { actionRefresh->setEnabled(false); }); + connect(queryHelper, &QueryHelper::finished, [this]() { actionRefresh->setEnabled(true); }); + + connect(tabWidget, SIGNAL(currentChanged(int)), this, SLOT(tabSwitched(int))); + connect(actionAdd, SIGNAL(triggered(bool)), this, SLOT(addServer())); + connect(actionDelete, SIGNAL(triggered(bool)), this, SLOT(deleteServer())); + connect(actionRefresh, SIGNAL(triggered(bool)), queryHelper, SLOT(refresh())); + connect(actionPlay, SIGNAL(triggered(bool)), this, SLOT(play())); + connect(tblServerBrowser, SIGNAL(clicked(QModelIndex)), this, SLOT(serverSelected())); + connect(tblFavorites, SIGNAL(clicked(QModelIndex)), this, SLOT(serverSelected())); + connect(tblFavorites, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(play())); + connect(tblServerBrowser, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(play())); + connect(cBoxNotFull, SIGNAL(toggled(bool)), this, SLOT(notFullSwitch(bool))); + connect(cBoxWithPlayers, SIGNAL(toggled(bool)), this, SLOT(havePlayersSwitch(bool))); + connect(cBBoxWOPass, SIGNAL(toggled(bool)), this, SLOT(noPasswordSwitch(bool))); + connect(comboLatency, SIGNAL(currentIndexChanged(int)), this, SLOT(maxLatencyChanged(int))); + connect(leGamemode, SIGNAL(textChanged(const QString &)), this, SLOT(gamemodeChanged(const QString &))); + + settingsWindow = new SettingsWindow(this); + + connect(actionSettings, &QAction::triggered, [&](bool v) { + settingsWindow->exec(); + }); + + SettingsMgr::get().loadBrowserSettings(*this); + MasterClient::get()->address(masterAddress); + + FIX_UNTIL(make_version(2, 0), "Favorites list should be implemented"); + { + tabWidget->removeTab(1); + //loadFavorites(); + } + queryHelper->refresh(); + + FIX_UNTIL(make_version(2, 0), "Account system should be implemented"); + { + //lblSignStatus = new QLabel("Not signed in"); + //statusBar()->addPermanentWidget(lblSignStatus); + actionAccount->setVisible(false); + for (auto action : toolBar->actions()) // hacky way to delete first separator + { + if (action->isSeparator()) + { + toolBar->removeAction(action); + break; + } + } + } + + lblBrowserVersion = new QLabel(QString::fromStdString(strVersion())); + + + statusBar()->addPermanentWidget(lblBrowserVersion); + + /*QPixmap img("/home/koncord/GH/banner.png"); + setMotdImage(&img, "https://master.tes3mp.com");*/ + + connect(MasterClient::get(), &MasterClient::latestVersion, [this](const QString &version){ + QStringList ver = version.split('.'); + int major = ver[0].toInt(); + int minor = ver[1].toInt(); + if (major > MAJOR_VERSION || minor > MINOR_VERSION) + setMotdHTML(R"(

Update for browser available

)"); + }); + MasterClient::get()->requestLatestVersionStr(); + +} + +MainWindow::~MainWindow() +{ + SettingsMgr::get().saveBrowserSettings(*this); + delete mGameInvoker; +} + +void MainWindow::addServerAndUpdate(const QString &addr) +{ + favorites->insertRow(0); + QModelIndex mi = favorites->index(0, Server::ADDR); + favorites->setData(mi, addr, Qt::EditRole); + /*auto address = addr.split(":"); + auto data = getExtendedData(address[0].toLatin1(), address[1].toUShort());*/ + + //NetController::get()->updateInfo(favorites, mi); + //QueryClient::Update(RakNet::SystemAddress()) + /*auto data = QueryClient::Get().Query(); + if (data.empty()) + return; + transform(data.begin(), data.end(), back_inserter());*/ +} + +void MainWindow::addServer() +{ + int id = tblServerBrowser->selectionModel()->currentIndex().row(); + + if (id >= 0) + { + int sourceId = proxyModel->mapToSource(proxyModel->index(id, Server::ADDR)).row(); + favorites->myData.push_back(browser->myData[sourceId]); + } +} + +void MainWindow::deleteServer() +{ + if (tabWidget->currentIndex() != 1) + return; + int id = tblFavorites->selectionModel()->currentIndex().row(); + if (id >= 0) + { + int sourceId = proxyModel->mapToSource(proxyModel->index(id, Server::ADDR)).row(); + favorites->removeRow(sourceId); + if (favorites->myData.isEmpty()) + { + actionPlay->setEnabled(false); + actionDelete->setEnabled(false); + } + } +} + +void MainWindow::play() +{ + QTableView *curTable = tabWidget->currentIndex() ? tblFavorites : tblServerBrowser; + int id = curTable->selectionModel()->currentIndex().row(); + if (id < 0) + return; + + + ServerModel *sm = ((ServerModel*)proxyModel->sourceModel()); + + int sourceId = proxyModel->mapToSource(proxyModel->index(id, Server::ADDR)).row(); + ServerInfoDialog infoDialog(sm->myData[sourceId], this); + + if (!infoDialog.exec()) + return; + + if (!infoDialog.isUpdated()) + return; + + QStringList arguments; + arguments.append(QString::fromStdString("--connect=" + sm->myData[sourceId].address).toLatin1()); + + if (sm->myData[sourceId].password) + { + bool ok; + QString passw = QInputDialog::getText(this, tr("Connecting to: ") + QString::fromStdString(sm->myData[sourceId].address), tr("Password: "), + QLineEdit::Password, "", &ok); + if (!ok) + return; + arguments.append(QLatin1String("--password=") + passw.toLatin1()); + } + + if (mGameInvoker->startProcess(QLatin1String("tes3mp"), arguments, true)) + return qApp->quit(); +} + +void MainWindow::tabSwitched(int index) +{ + if (index == 0) + { + proxyModel->setSourceModel(browser); + actionDelete->setEnabled(false); + } + else + { + proxyModel->setSourceModel(favorites); + } + actionPlay->setEnabled(false); + actionAdd->setEnabled(false); +} + +void MainWindow::serverSelected() +{ + actionPlay->setEnabled(true); + if (tabWidget->currentIndex() == 0) + actionAdd->setEnabled(true); + if (tabWidget->currentIndex() == 1) + actionDelete->setEnabled(true); +} + +void MainWindow::closeEvent(QCloseEvent *event) +{ + Files::ConfigurationManager cfgMgr; + QString cfgPath = QString::fromStdString((cfgMgr.getUserConfigPath() / "favorites.dat").string()); + + QJsonArray saveData; + for (auto server : favorites->myData) + saveData.push_back(QString::fromStdString(server.address)); + + QFile file(cfgPath); + + if (!file.open(QIODevice::WriteOnly)) + { + qDebug() << "Cannot save " << cfgPath; + return; + } + + file.write(QJsonDocument(saveData).toJson()); + file.close(); +} + + +void MainWindow::loadFavorites() +{ + Files::ConfigurationManager cfgMgr; + QString cfgPath = QString::fromStdString((cfgMgr.getUserConfigPath() / "favorites.dat").string()); + + QFile file(cfgPath); + if (!file.open(QIODevice::ReadOnly)) + { + qDebug() << "Cannot open " << cfgPath; + return; + } + + QJsonDocument jsonDoc(QJsonDocument::fromJson(file.readAll())); + + for (auto server : jsonDoc.array()) + addServerAndUpdate(server.toString()); + + file.close(); +} + +void MainWindow::notFullSwitch(bool state) +{ + proxyModel->filterFullServer(state); +} + +void MainWindow::havePlayersSwitch(bool state) +{ + proxyModel->filterEmptyServers(state); +} + +void MainWindow::noPasswordSwitch(bool state) +{ + proxyModel->filterPassworded(state); +} + +void MainWindow::maxLatencyChanged(int index) +{ + int maxLatency = index * 50; + proxyModel->pingLessThan(maxLatency); + +} + +void MainWindow::gamemodeChanged(const QString &text) +{ + proxyModel->setFilterFixedString(text); + proxyModel->setFilterKeyColumn(Server::MODNAME); +} + + +void MainWindow::setMotdHTML(const QString &data) +{ + auto lbl = new QLabel(this); + + lbl->setText(data); + lbl->setTextInteractionFlags(Qt::TextBrowserInteraction); + lbl->setOpenExternalLinks(true); + lbl->setTextFormat(Qt::RichText); + lbl->setAlignment(Qt::AlignCenter); + + setMotdWidget(lbl); +} + +void MainWindow::setMotdImage(QPixmap *image, const QString &link) +{ + auto btn = new ImageButton(this); + + if (image == nullptr) + { + btn->deleteLater(); + if (!link.isEmpty()) + setMotdHTML("

" + link + "

"); + return; + } + + btn->setPixmap(image->scaled(728, 60)); + + connect(btn, &QAbstractButton::released, this, [link]() { + QDesktopServices::openUrl(link); + }); + + setMotdWidget(btn); +} + +void MainWindow::setMotdWidget(QWidget *widget) +{ + motdWidget->deleteLater(); + motdWidget = widget; + motdWidget->setFixedSize(QSize(728, 60)); + motdLayout->addWidget(motdWidget, 0, Qt::AlignHCenter); +} diff --git a/apps/browser/MainWindow.hpp b/apps/browser/MainWindow.hpp new file mode 100644 index 0000000..8eaac44 --- /dev/null +++ b/apps/browser/MainWindow.hpp @@ -0,0 +1,53 @@ +// +// Created by koncord on 06.01.17. +// + +#pragma once + +#include "ui_Main.h" +#include "ServerModel.hpp" +#include "MySortFilterProxyModel.hpp" +#include + +class QueryHelper; +class SettingsWindow; +class SettingsMgr; + +class MainWindow : public QMainWindow, private Ui::MainWindow +{ + Q_OBJECT + friend class SettingsMgr; +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; +protected: + void closeEvent(QCloseEvent * event) Q_DECL_OVERRIDE; + void addServerAndUpdate(const QString &addr); +protected slots: + void tabSwitched(int index); + void addServer(); + void deleteServer(); + void play(); + void serverSelected(); + void notFullSwitch(bool state); + void havePlayersSwitch(bool state); + void noPasswordSwitch(bool state); + void maxLatencyChanged(int index); + void gamemodeChanged(const QString &text); + + void setMotdHTML(const QString &data); + void setMotdImage(QPixmap *image, const QString &link); + +private: + void setMotdWidget(QWidget *widget); +private: + SettingsWindow *settingsWindow; + QueryHelper *queryHelper; + Process::ProcessInvoker *mGameInvoker; + ServerModel *browser, *favorites; + MySortFilterProxyModel *proxyModel; + void loadFavorites(); + QLabel *lblSignStatus; + QLabel *lblBrowserVersion; + QString masterAddress; +}; diff --git a/apps/browser/MySortFilterProxyModel.cpp b/apps/browser/MySortFilterProxyModel.cpp new file mode 100644 index 0000000..25347fd --- /dev/null +++ b/apps/browser/MySortFilterProxyModel.cpp @@ -0,0 +1,84 @@ +// +// Created by koncord on 30.01.17. +// + +#include "MySortFilterProxyModel.hpp" +#include "ServerModel.hpp" + +#include +#include + +bool MySortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + + QModelIndex pingIndex = sourceModel()->index(sourceRow, Server::PING, sourceParent); + QModelIndex plIndex = sourceModel()->index(sourceRow, Server::PLAYERS, sourceParent); + QModelIndex maxPlIndex = sourceModel()->index(sourceRow, Server::MAX_PLAYERS, sourceParent); + QModelIndex passwordIndex = sourceModel()->index(sourceRow, Server::PASSW, sourceParent); + + bool pingOk; + int ping = sourceModel()->data(pingIndex).toInt(&pingOk); + int players = sourceModel()->data(plIndex).toInt(); + int maxPlayers = sourceModel()->data(maxPlIndex).toInt(); + + if (maxPing > 0 && (ping == -1 || ping > maxPing || !pingOk)) + return false; + if (filterEmpty && players == 0) + return false; + if (filterFull && players >= maxPlayers) + return false; + if(filterPasswEnabled && sourceModel()->data(passwordIndex).toString() == "Yes") + return false; + + return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); +} + +bool MySortFilterProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const +{ + if(sortColumn() == Server::PING) + { + bool valid; + + int pingright = sourceModel()->data(source_right).toInt(&valid); + pingright = valid ? pingright : PING_UNREACHABLE; + + int pingleft = sourceModel()->data(source_left).toInt(&valid); + pingleft = valid ? pingleft : PING_UNREACHABLE; + return pingleft < pingright; + } + else + return QSortFilterProxyModel::lessThan(source_left, source_right); +} + +MySortFilterProxyModel::MySortFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + filterEmpty = false; + filterFull = false; + filterPasswEnabled = false; + maxPing = 0; + setSortCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); +} + +void MySortFilterProxyModel::filterEmptyServers(bool state) +{ + filterEmpty = state; + invalidateFilter(); +} + +void MySortFilterProxyModel::filterFullServer(bool state) +{ + filterFull = state; + invalidateFilter(); +} + +void MySortFilterProxyModel::pingLessThan(int maxPing) +{ + this->maxPing = maxPing; + invalidateFilter(); +} + +void MySortFilterProxyModel::filterPassworded(bool state) +{ + filterPasswEnabled = state; + invalidateFilter(); +} diff --git a/apps/browser/MySortFilterProxyModel.hpp b/apps/browser/MySortFilterProxyModel.hpp new file mode 100644 index 0000000..385f196 --- /dev/null +++ b/apps/browser/MySortFilterProxyModel.hpp @@ -0,0 +1,24 @@ +// +// Created by koncord on 30.01.17. +// + +#pragma once + +#include + +class MySortFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const Q_DECL_FINAL; + bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const Q_DECL_FINAL; +public: + explicit MySortFilterProxyModel(QObject *parent); + void filterFullServer(bool state); + void filterEmptyServers(bool state); + void filterPassworded(bool state); + void pingLessThan(int maxPing); +private: + bool filterEmpty, filterFull, filterPasswEnabled; + int maxPing; +}; diff --git a/apps/browser/PingHelper.cpp b/apps/browser/PingHelper.cpp new file mode 100644 index 0000000..c11b4c0 --- /dev/null +++ b/apps/browser/PingHelper.cpp @@ -0,0 +1,56 @@ +// +// Created by koncord on 03.05.17. +// + +#include "PingHelper.hpp" +#include "ServerModel.hpp" +#include +#include "PingUpdater.hpp" + +void PingHelper::Add(int row, const AddrPair &addrPair) +{ + pingUpdater->addServer(row, addrPair); + if (!pingThread->isRunning()) + pingThread->start(); +} + +void PingHelper::Reset() +{ + //if (pingThread->isRunning()) + Stop(); +} + +void PingHelper::Stop() +{ + emit pingUpdater->stop(); +} + +void PingHelper::SetModel(QAbstractTableModel *model) +{ + this->model = model; +} + +void PingHelper::update(int row, unsigned ping) +{ + model->setData(model->index(row, Server::PING), ping); +} + +PingHelper &PingHelper::Get() +{ + static PingHelper helper; + return helper; +} + +PingHelper::PingHelper() : QObject() +{ + pingThread = new QThread; + pingUpdater = new PingUpdater; + pingUpdater->moveToThread(pingThread); + + connect(pingThread, &QThread::started, pingUpdater, &PingUpdater::process); + connect(pingUpdater, &PingUpdater::start, pingThread, &QThread::start); + connect(pingUpdater, &PingUpdater::finished, pingThread, &QThread::quit); + connect(this, &PingHelper::stop, pingUpdater, &PingUpdater::stop); + //connect(pingUpdater, SIGNAL(finished()), pingUpdater, SLOT(deleteLater())); + connect(pingUpdater, &PingUpdater::updateModel, this, &PingHelper::update); +} diff --git a/apps/browser/PingHelper.hpp b/apps/browser/PingHelper.hpp new file mode 100644 index 0000000..575bdef --- /dev/null +++ b/apps/browser/PingHelper.hpp @@ -0,0 +1,38 @@ +// +// Created by koncord on 03.05.17. +// + +#pragma once + +#include +#include +#include +#include "Types.hpp" + +class PingUpdater; + +class PingHelper : public QObject +{ + Q_OBJECT +public: + + void Reset(); + void Add(int row, const AddrPair &addrPair); + void Stop(); + void SetModel(QAbstractTableModel *model); + //void UpdateImmedialy(PingUpdater::AddrPair addrPair); + static PingHelper &Get(); + + PingHelper(const PingHelper&) = delete; + PingHelper& operator=(const PingHelper&) = delete; +private: + PingHelper(); +signals: + void stop(); +public slots: + void update(int row, unsigned ping); +private: + QThread *pingThread; + PingUpdater *pingUpdater; + QAbstractTableModel *model; +}; diff --git a/apps/browser/PingUpdater.cpp b/apps/browser/PingUpdater.cpp new file mode 100644 index 0000000..befea9c --- /dev/null +++ b/apps/browser/PingUpdater.cpp @@ -0,0 +1,50 @@ +// +// Created by koncord on 02.05.17. +// + +#include "PingUpdater.hpp" +#include "netutils/Utils.hpp" +#include +#include +#include + +void PingUpdater::stop() +{ + servers.clear(); + run = false; +} + +void PingUpdater::addServer(int row, const AddrPair &addr) +{ + servers.push_back({row, addr}); + run = true; + emit start(); +} + +void PingUpdater::process() +{ + while (run) + { + if (servers.count() == 0) + { + QThread::msleep(1000); + if (servers.count() == 0) + { + qDebug() << "PingUpdater stopped due to inactivity"; + run = false; + continue; + } + } + + ServerRow server = servers.back(); + servers.pop_back(); + + unsigned ping = PingRakNetServer(server.second.first.toLatin1(), server.second.second); + + qDebug() << "Pong from" << server.second.first + "|" + QString::number(server.second.second) + << ":" << ping << "ms" << "Sizeof servers: " << servers.size(); + + emit updateModel(server.first, ping); + } + emit finished(); +} diff --git a/apps/browser/PingUpdater.hpp b/apps/browser/PingUpdater.hpp new file mode 100644 index 0000000..9dffcf2 --- /dev/null +++ b/apps/browser/PingUpdater.hpp @@ -0,0 +1,29 @@ +// +// Created by koncord on 02.05.17. +// + +#pragma once + +#include +#include +#include + +#include "Types.hpp" + +class PingUpdater : public QObject +{ + Q_OBJECT +public: + PingUpdater(QObject *parent = 0): QObject(parent) {} + void addServer(int row, const AddrPair &addrPair); +public slots: + void stop(); + void process(); +signals: + void start(QThread::Priority priority = QThread::InheritPriority); + void updateModel(int row, unsigned ping); + void finished(); +private: + QVector servers; + bool run; +}; diff --git a/apps/browser/QueryHelper.cpp b/apps/browser/QueryHelper.cpp new file mode 100644 index 0000000..ccfc0f1 --- /dev/null +++ b/apps/browser/QueryHelper.cpp @@ -0,0 +1,73 @@ +// +// Created by koncord on 27.05.17. +// + +#include "netutils/MasterClient.hpp" +#include "netutils/Utils.hpp" +#include "ServerModel.hpp" +#include "QueryHelper.hpp" +#include "PingHelper.hpp" + +QueryHelper::QueryHelper(QAbstractItemModel *model) +{ + _model = model; + + connect(MasterClient::get(), &MasterClient::server, this, &QueryHelper::update); + connect(MasterClient::get(), &MasterClient::finished, this, &QueryHelper::finished); +} + +void QueryHelper::refresh() +{ + + //if (!queryThread->isRunning()) + { + _model->removeRows(0, _model->rowCount()); + PingHelper::Get().Stop(); + MasterClient::get()->requestList(); + //queryThread->start(); + emit started(); + } +} + +void QueryHelper::terminate() +{ + +} + +void QueryHelper::update(const Server& data) +{ + ServerModel *model = ((ServerModel*)_model); + model->insertRow(model->rowCount()); + int row = model->rowCount() - 1; + + QModelIndex mi = model->index(row, Server::ADDR); + model->setData(mi, QString::fromStdString(data.address)); + + mi = model->index(row, Server::PORT); + model->setData(mi, data.port); + + mi = model->index(row, Server::ID); + model->setData(mi, QString::fromStdString(data.id)); + + mi = model->index(row, Server::PLAYERS); + model->setData(mi, data.players); + + mi = model->index(row, Server::MAX_PLAYERS); + model->setData(mi, data.maxPlayers); + + mi = model->index(row, Server::HOSTNAME); + model->setData(mi, QString::fromStdString(data.hostname)); + + mi = model->index(row, Server::MODNAME); + model->setData(mi, QString::fromStdString(data.modname)); + + mi = model->index(row, Server::VERSION); + model->setData(mi, QString::fromStdString(data.version)); + + mi = model->index(row, Server::PASSW); + model->setData(mi, data.password); + + mi = model->index(row, Server::PING); + model->setData(mi, PING_UNREACHABLE); + PingHelper::Get().Add(row, {QString::fromStdString(data.address), data.port}); +} diff --git a/apps/browser/QueryHelper.hpp b/apps/browser/QueryHelper.hpp new file mode 100644 index 0000000..60dc6d0 --- /dev/null +++ b/apps/browser/QueryHelper.hpp @@ -0,0 +1,28 @@ +// +// Created by koncord on 27.05.17. +// + +#pragma once + +#include +#include +#include +#include "Data.hpp" + +class QueryHelper : public QObject +{ +Q_OBJECT +public: + explicit QueryHelper(QAbstractItemModel *model); +public slots: + void refresh(); + void terminate(); +private slots: + void update(const Server& data); +signals: + void finished(); + void started(); +private: + QAbstractItemModel *_model; +}; + diff --git a/apps/browser/ServerInfoDialog.cpp b/apps/browser/ServerInfoDialog.cpp new file mode 100644 index 0000000..9c12247 --- /dev/null +++ b/apps/browser/ServerInfoDialog.cpp @@ -0,0 +1,140 @@ +// +// Created by koncord on 07.01.17. +// + +#include "qdebug.h" + +#include "ServerInfoDialog.hpp" +#include +#include +#include +#include "netutils/MasterClient.hpp" +#include "PingUpdater.hpp" + +using namespace std; +using namespace RakNet; + +ServerInfoDialog::ServerInfoDialog(const Server &server, QWidget *parent): QDialog(parent), serverData(server) +{ + setupUi(this); + + pingUpdater = new PingUpdater; + + connect(MasterClient::get(), &MasterClient::singleServer, this, &ServerInfoDialog::updateServer); + connect(btnRefresh, &QPushButton::clicked, this, &ServerInfoDialog::refresh); + + FIX_UNTIL(make_version(2, 0), "Account system should be implemented"); + { + lblstaT3MPMID->hide(); + leServerId->hide(); + + lblstaReqT3MPMAcc->hide(); + cbReqT3MPMAcc->hide(); + } + + FIX_UNTIL(make_version(3, 0), "Download system should be implemented"); + { + lblstaHasDLServ->hide(); + cbHasDLServer->hide(); + } + btnConnect->setDisabled(true); + setLogo(nullptr); + initPingUpdater(); +} + +void ServerInfoDialog::initPingUpdater() +{ + pingThread = new QThread; + pingUpdater = new PingUpdater; + pingUpdater->moveToThread(pingThread); + + connect(pingThread, &QThread::started, pingUpdater, &PingUpdater::process); + connect(pingUpdater, &PingUpdater::start, pingThread, &QThread::start); + connect(pingUpdater, &PingUpdater::finished, pingThread, &QThread::quit); + //connect(pingUpdater, SIGNAL(finished()), pingUpdater, SLOT(deleteLater())); + connect(pingUpdater, &PingUpdater::updateModel, [this](int row, unsigned ping) { + if (ping != PING_UNREACHABLE) + this->lblPing->setText(QString::number(ping)); + else + this->lblPing->setText(tr("Unreachable")); + btnConnect->setDisabled(ping == PING_UNREACHABLE); + }); +} + +ServerInfoDialog::~ServerInfoDialog() +{ + pingUpdater->stop(); +} + +bool ServerInfoDialog::isUpdated() +{ + return !serverData.id.empty(); +} + +void ServerInfoDialog::setLogo(QPixmap *image) +{ + QPixmap scaled; + if (image == nullptr) + { + QPixmap pic(":/browser/tes3mp_logo.png"); + scaled = pic.scaled(192, 192, Qt::IgnoreAspectRatio, Qt::FastTransformation); + } + else + scaled = image->scaled(192, 192, Qt::IgnoreAspectRatio, Qt::FastTransformation); + + QPalette palette; + palette.setBrush(backgroundRole(), QBrush(scaled)); + + logoFrame->setAutoFillBackground(true); + logoFrame->setPalette(palette); + logoFrame->show(); +} + +void ServerInfoDialog::refresh() +{ + MasterClient::get()->requestServer(serverData); +} + +int ServerInfoDialog::exec() +{ + refresh(); + return QDialog::exec(); +} + +void ServerInfoDialog::setPlayersCnt(int players, int _maxPlayers) +{ + if(_maxPlayers != -1) + maxPlayers = _maxPlayers; + lblPlayers->setText(QString::number(players) + " / " + QString::number(maxPlayers)); +} + + +void ServerInfoDialog::updateServer(Server server, ServerExtra extra) +{ + qDebug() << "updateServer"; + lblName->setText(QString::fromStdString(server.hostname) + "\t" + QString::fromStdString(server.modname)); + leServerId->setText(QString::fromStdString(server.id)); + leAddr->setText(QString::fromStdString(server.address) + ":" + QString::number(server.port)); + setPlayersCnt(server.players, server.maxPlayers); + + cbHasDLServer->setChecked(!extra.dlServer.empty()); + + listPlayers->clear(); + for (const auto &player : extra.players) // todo: optimize it + listPlayers->addItem(QString::fromStdString(player)); + + setPlayersCnt(extra.players.size()); + + listPlugins->clear(); + for (const auto &plugin : extra.plugins) + listPlugins->addItem(QString::fromStdString(plugin)); + + listRules->clear(); + for (const auto &[key, value] : extra.extraInfo) + { + std::string ruleStr = key; + ruleStr += " : "; + ruleStr += value; + listRules->addItem(QString::fromStdString(ruleStr)); + } +} diff --git a/apps/browser/ServerInfoDialog.hpp b/apps/browser/ServerInfoDialog.hpp new file mode 100644 index 0000000..2a605c2 --- /dev/null +++ b/apps/browser/ServerInfoDialog.hpp @@ -0,0 +1,39 @@ +// +// Created by koncord on 07.01.17. +// + +#pragma once + +#include "ui_ServerInfo.h" +#include +#include +#include "Data.hpp" + +class PingUpdater; + +class ServerInfoDialog : public QDialog, public Ui::Dialog +{ + Q_OBJECT +public: + explicit ServerInfoDialog(const Server &server, QWidget *parent = nullptr); + ~ServerInfoDialog() override; + bool isUpdated(); +public slots: + void refresh(); + int exec() Q_DECL_OVERRIDE; + void setLogo(QPixmap *image); + void updateServer(Server server, ServerExtra extra); +signals: + void updatedInfo(const Server &server, const ServerExtra &extra); +private: + void initPingUpdater(); + void setPlayersCnt(int players, int maxPlayers = -1); +private: + Server serverData; + ServerExtra extraData; + + PingUpdater *pingUpdater; + QThread *pingThread; + + int maxPlayers; +}; diff --git a/apps/browser/ServerModel.cpp b/apps/browser/ServerModel.cpp new file mode 100644 index 0000000..a0acc41 --- /dev/null +++ b/apps/browser/ServerModel.cpp @@ -0,0 +1,210 @@ +#include +#include "ServerModel.hpp" +#include +#include + +ServerModel::ServerModel(QObject *parent) : QAbstractTableModel(parent) +{ +} +/*QHash ServerModel::roleNames() const +{ + return roles; +}*/ + +QVariant ServerModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + if (index.row() < 0 || index.row() > myData.size()) + return QVariant(); + + const Server &sd = myData.at(index.row()); + + if (role == Qt::DisplayRole) + { + QVariant var; + switch (index.column()) + { + case Server::ADDR: + var = QString::fromStdString(sd.address); + break; + case Server::PORT: + var = sd.port; + break; + case Server::ID: + var = QString::fromStdString(sd.id); + break; + case Server::PASSW: + var = sd.password ? "Yes" : "No"; + break; + case Server::VERSION: + var = QString::fromStdString(sd.version); + break; + case Server::PLAYERS: + var = sd.players; + break; + case Server::MAX_PLAYERS: + var = sd.maxPlayers; + break; + case Server::HOSTNAME: + var = QString::fromStdString(sd.hostname); + break; + case Server::PING: + var = sd.ping == PING_UNREACHABLE ? QVariant("Unreachable") : sd.ping; + break; + case Server::MODNAME: + if (sd.modname.empty()) + var = "Default"; + else + var = QString::fromStdString(sd.modname); + break; + } + return var; + } + return QVariant(); +} + +QVariant ServerModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + QVariant var; + if (orientation == Qt::Horizontal) + { + if (role == Qt::SizeHintRole) + { + /*if (section == ServerData::HOSTNAME) + var = QSize(200, 25);*/ + } + else if (role == Qt::DisplayRole) + { + + switch (section) + { + case Server::ADDR: + var = "Address"; + break; + case Server::PORT: + var = "Port"; + break; + case Server::ID: + var = "Persistent id"; + break; + case Server::PASSW: + var = "Password"; + break; + case Server::VERSION: + var = "Version"; + break; + case Server::HOSTNAME: + var = "Host name"; + break; + case Server::PLAYERS: + var = "Players"; + break; + case Server::MAX_PLAYERS: + var = "Max players"; + break; + case Server::PING: + var = "Ping"; + break; + case Server::MODNAME: + var = "Game mode"; + } + } + } + return var; +} + +int ServerModel::rowCount(const QModelIndex &parent) const +{ + return myData.size(); +} + +int ServerModel::columnCount(const QModelIndex &parent) const +{ + return Server::LAST; +} + +bool ServerModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.isValid() && role == Qt::EditRole) + { + int row = index.row(); + int col = index.column(); + + Server &sd = myData[row]; + bool ok = true; + switch(col) + { + case Server::ADDR: + sd.address = value.toString().toStdString(); + ok = !sd.address.empty(); + break; + case Server::PORT: + sd.port = static_cast(value.toUInt(&ok)); + break; + case Server::ID: + sd.id = value.toString().toStdString(); + ok = !sd.id.empty(); + break; + case Server::PASSW: + sd.password = value.toBool(); + break; + case Server::VERSION: + sd.version = value.toString().toStdString(); + ok = !sd.address.empty(); + break; + case Server::PLAYERS: + sd.players = value.toUInt(&ok); + break; + case Server::MAX_PLAYERS: + sd.maxPlayers = value.toUInt(&ok); + break; + case Server::HOSTNAME: + sd.hostname = value.toString().toStdString(); + ok = !sd.address.empty(); + break; + case Server::PING: + sd.ping = value.toInt(&ok); + break; + case Server::MODNAME: + sd.modname = value.toString().toStdString(); + break; + default: + return false; + } + if (ok) + emit(dataChanged(index, index)); + return true; + } + return false; +} + +bool ServerModel::insertRows(int position, int count, const QModelIndex &index) +{ + Q_UNUSED(index); + beginInsertRows(QModelIndex(), position, position + count - 1); + + myData.insert(position, count, {}); + + endInsertRows(); + return true; +} + +bool ServerModel::removeRows(int position, int count, const QModelIndex &parent) +{ + if (count == 0) + return false; + + beginRemoveRows(parent, position, position + count - 1); + myData.erase(myData.begin()+position, myData.begin() + position + count); + endRemoveRows(); + + return true; +} + +QModelIndex ServerModel::index(int row, int column, const QModelIndex &parent) const +{ + + QModelIndex index = QAbstractTableModel::index(row, column, parent); + return index; +} diff --git a/apps/browser/ServerModel.hpp b/apps/browser/ServerModel.hpp new file mode 100644 index 0000000..3a0633c --- /dev/null +++ b/apps/browser/ServerModel.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include +#include "Data.hpp" + + +class ServerModel: public QAbstractTableModel +{ + Q_OBJECT +public: + explicit ServerModel(QObject *parent = nullptr); + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_FINAL; + int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_FINAL; + int columnCount(const QModelIndex &parent) const Q_DECL_FINAL; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) Q_DECL_FINAL; + + bool insertRows(int row, int count, const QModelIndex &index = QModelIndex()) Q_DECL_FINAL; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) Q_DECL_FINAL; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_FINAL; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_FINAL; + + +public: + //QHash roles; + QVector myData; +}; diff --git a/apps/browser/Settings.cpp b/apps/browser/Settings.cpp new file mode 100644 index 0000000..de57247 --- /dev/null +++ b/apps/browser/Settings.cpp @@ -0,0 +1,196 @@ +// +// Created by koncord on 31.03.18. +// + +#include "Settings.hpp" +#include + +#include +#include "SettingsWindow.hpp" +#include "MainWindow.hpp" + +std::string loadSettings (Settings::Manager & settings, const std::string &cfgName) +{ + Files::ConfigurationManager mCfgMgr; + // Create the settings manager and load default settings file + const std::string localdefault = (mCfgMgr.getLocalPath() / (cfgName + "-default.cfg")).string(); + const std::string globaldefault = (mCfgMgr.getGlobalPath() / (cfgName + "-default.cfg")).string(); + + // prefer local + if (boost::filesystem::exists(localdefault)) + settings.loadDefault(localdefault); + else if (boost::filesystem::exists(globaldefault)) + settings.loadDefault(globaldefault); + else + throw std::runtime_error ("No default settings file found! Make sure the file \"" + cfgName + "-default.cfg\" was properly installed."); + + // load user settings if they exist + const std::string settingspath = (mCfgMgr.getUserConfigPath() / (cfgName + ".cfg")).string(); + if (boost::filesystem::exists(settingspath)) + settings.loadUser(settingspath); + + return settingspath; +} + +SettingsMgr &SettingsMgr::get() +{ + static SettingsMgr mgr; + return mgr; +} + +SettingsMgr::SettingsMgr() +{ + clientCfg = loadSettings(clientMgr, "tes3mp-client"); + serverCfg = loadSettings(serverMgr, "tes3mp-server"); + browserCfg = loadSettings(browserMgr, "tes3mp-browser"); +} + +void SettingsMgr::loadBrowserSettings(MainWindow &mw) +{ + mw.masterAddress = QString::fromStdString(browserMgr.getString("address", "Master")); + mw.comboLatency->setCurrentIndex(browserMgr.getInt("maxLatency", "Browser")); + mw.leGamemode->setText(QString::fromStdString(browserMgr.getString("gameMode", "Browser"))); + mw.cBoxNotFull->setCheckState(browserMgr.getBool("notFull", "Browser") ? Qt::Checked : Qt::Unchecked); + mw.cBoxWithPlayers->setCheckState(browserMgr.getBool("withPlayers", "Browser") ? Qt::Checked : Qt::Unchecked); + mw.cBBoxWOPass->setCheckState(browserMgr.getBool("noPassword", "Browser") ? Qt::Checked : Qt::Unchecked); + + mw.tblServerBrowser->sortByColumn(browserMgr.getInt("sortByCol", "Browser"), + browserMgr.getBool("sortByColAscending", "Browser") ? Qt::AscendingOrder : Qt::DescendingOrder); + +} + +void SettingsMgr::saveBrowserSettings(MainWindow &mw) +{ + browserMgr.setInt("maxLatency", "Browser", mw.comboLatency->currentIndex()); + browserMgr.setString("gameMode", "Browser", mw.leGamemode->text().toStdString()); + browserMgr.setBool("notFull", "Browser", mw.cBoxNotFull->checkState() == Qt::Checked); + browserMgr.setBool("withPlayers", "Browser", mw.cBoxWithPlayers->checkState() == Qt::Checked); + browserMgr.setBool("noPassword", "Browser", mw.cBBoxWOPass->checkState() == Qt::Checked); + + browserMgr.setInt("sortByCol", "Browser", mw.tblServerBrowser->horizontalHeader()->sortIndicatorSection()); + browserMgr.setBool("sortByColAscending", "Browser", mw.tblServerBrowser->horizontalHeader()->sortIndicatorOrder() == Qt::AscendingOrder); + + browserMgr.saveUser(browserCfg); +} + +void SettingsMgr::loadClientSettings(Ui::DialogSettings &mw) +{ + /*mw.leClientAddress->setText(QString::fromStdString(clientMgr.getString("destinationAddress", "General"))); + mw.leClientPort->setText(QString::fromStdString(clientMgr.getString("port", "General"))); + mw.leClientPassword->setText(QString::fromStdString(clientMgr.getString("password", "General"))); + mw.combLoglevel->setCurrentIndex(clientMgr.getInt("loglevel", "General")); + + mw.leClientMAddress->setText(QString::fromStdString(clientMgr.getString("address", "Master"))); + mw.leClientMPort->setText(QString::fromStdString(clientMgr.getString("port", "Master"))); + + mw.pbChatKey->setText(QString::fromStdString(clientMgr.getString("keySay", "Chat"))); + mw.pbModeKey->setText(QString::fromStdString(clientMgr.getString("keyChatMode", "Chat"))); + + mw.sbPosX->setValue(clientMgr.getInt("x", "Chat")); + mw.sbPosY->setValue(clientMgr.getInt("y", "Chat")); + mw.sbPosW->setValue(clientMgr.getInt("w", "Chat")); + mw.sbPosH->setValue(clientMgr.getInt("h", "Chat")); + + mw.sbDelay->setValue(clientMgr.getFloat("delay", "Chat"));*/ +} + +void SettingsMgr::saveClientSettings(Ui::DialogSettings &mw) +{ + clientMgr.setString("destinationAddress", "General", mw.leClientAddress->text().toStdString()); + clientMgr.setString("port", "General", mw.leClientPort->text().toStdString()); + clientMgr.setString("password", "General", mw.leClientPassword->text().toStdString()); + clientMgr.setInt("loglevel", "General", mw.combLoglevel->currentIndex()); + + clientMgr.setString("keySay", "Chat", mw.pbChatKey->text().toStdString()); + clientMgr.setString("keyChatMode", "Chat", mw.pbModeKey->text().toStdString()); + + clientMgr.setInt("x", "Chat", mw.sbPosX->value()); + clientMgr.setInt("y", "Chat", mw.sbPosY->value()); + clientMgr.setInt("w", "Chat", mw.sbPosW->value()); + clientMgr.setInt("h", "Chat", mw.sbPosH->value()); + + clientMgr.setFloat("delay", "Chat", mw.sbDelay->value()); + + clientMgr.saveUser(clientCfg); + +} + +bool SettingsMgr::isPreRewrite() +{ + static int preRewrite = -1; + + if (preRewrite == -1) + { + preRewrite = 0; + + try + { + serverMgr.getString("home", "Modules"); + } + catch(...) + { + preRewrite = 1; + } + } + + return preRewrite == 1; +} + +void SettingsMgr::loadServerSettings(Ui::DialogSettings &mw) +{ + mw.leServerAddress->setText(QString::fromStdString(serverMgr.getString("localAddress", "General"))); + mw.leServerPort->setText(QString::fromStdString(serverMgr.getString("port", "General"))); + mw.sbMaxPlayers->setValue(serverMgr.getInt("maximumPlayers", "General")); + mw.leHostname->setText(QString::fromStdString(serverMgr.getString("hostname", "General"))); + mw.combServerLoglevel->setCurrentIndex(serverMgr.getInt("logLevel", "General")); + mw.leServerPassword->setText(QString::fromStdString(serverMgr.getString("password", "General"))); + + + if (!isPreRewrite()) + { + mw.leModulePath->setText(QString::fromStdString(serverMgr.getString("home", "Modules"))); + mw.chbAutosort->setCheckState(serverMgr.getBool("autoSort", "Modules") ? Qt::Checked : Qt::Unchecked); + } + else + { + mw.chbAutosort->setVisible(false); + mw.gbModules->setTitle("Plugins"); + + QString pluginsStr = QString::fromStdString(serverMgr.getString("plugins", "Plugins")); + mw.listModules->addItems(pluginsStr.split(",", QString::SkipEmptyParts)); + + mw.leModulePath->setText(QString::fromStdString(serverMgr.getString("home", "Plugins"))); + } + +} + +void SettingsMgr::saveServerSettings(Ui::DialogSettings &mw) +{ + serverMgr.setString("localAddress", "General", mw.leServerAddress->text().toStdString()); + serverMgr.setString("port", "General", mw.leServerPort->text().toStdString()); + serverMgr.setInt("maximumPlayers", "General", mw.sbMaxPlayers->value()); + serverMgr.setString("hostname", "General", mw.leHostname->text().toStdString()); + serverMgr.setInt("logLevel", "General", mw.combServerLoglevel->currentIndex()); + serverMgr.setString("password", "General", mw.leServerPassword->text().toStdString()); + + if(!isPreRewrite()) + { + serverMgr.setString("home", "Modules", mw.leModulePath->text().toStdString()); + serverMgr.setBool("autoSort", "Modules", mw.chbAutosort->checkState() == Qt::Checked); + } + else + { + QString plugins; + for(int i = 0; i < mw.listModules->count(); ++i) + { + plugins += mw.listModules->item(i)->text(); + if (i < mw.listModules->count() - 1) + plugins += ","; + } + + serverMgr.setString("plugins", "Plugins", plugins.toStdString()); + serverMgr.setString("home", "Plugins", mw.leModulePath->text().toStdString()); + } + + serverMgr.saveUser(serverCfg); +} diff --git a/apps/browser/Settings.hpp b/apps/browser/Settings.hpp new file mode 100644 index 0000000..350e0f4 --- /dev/null +++ b/apps/browser/Settings.hpp @@ -0,0 +1,34 @@ +// +// Created by koncord on 31.03.18. +// + +#pragma once + +#include + +namespace Ui +{ + class DialogSettings; +} +class MainWindow; + +class SettingsMgr +{ +public: + static SettingsMgr &get(); + void loadBrowserSettings(MainWindow &mw); + void saveBrowserSettings(MainWindow &mw); + + void loadClientSettings(Ui::DialogSettings &mw); + void saveClientSettings(Ui::DialogSettings &mw); + + void loadServerSettings(Ui::DialogSettings &mw); + void saveServerSettings(Ui::DialogSettings &mw); + + bool isPreRewrite(); + +private: + SettingsMgr(); + Settings::Manager serverMgr, clientMgr, browserMgr; + std::string serverCfg, clientCfg, browserCfg; +}; diff --git a/apps/browser/SettingsWindow.cpp b/apps/browser/SettingsWindow.cpp new file mode 100644 index 0000000..b55ab30 --- /dev/null +++ b/apps/browser/SettingsWindow.cpp @@ -0,0 +1,29 @@ +// +// Created by koncord on 18.07.18. +// + +#include +#include "SettingsWindow.hpp" +#include "Settings.hpp" + +SettingsWindow::SettingsWindow(QWidget *parent): + QDialog(parent) +{ + setupUi(this); + + connect(pbModulePath, &QPushButton::clicked, [this](bool) { + QString str = QFileDialog::getExistingDirectory(this, tr("Module path"), + leModulePath->text(), QFileDialog::ShowDirsOnly); + if(!str.isEmpty()) + leModulePath->setText(str); + }); + + SettingsMgr::get().loadClientSettings(*this); + SettingsMgr::get().loadServerSettings(*this); +} + +SettingsWindow::~SettingsWindow() +{ + SettingsMgr::get().saveClientSettings(*this); + SettingsMgr::get().saveServerSettings(*this); +} diff --git a/apps/browser/SettingsWindow.hpp b/apps/browser/SettingsWindow.hpp new file mode 100644 index 0000000..e218bf6 --- /dev/null +++ b/apps/browser/SettingsWindow.hpp @@ -0,0 +1,20 @@ +// +// Created by koncord on 18.07.18. +// + +#pragma once + +#include +#include "ui_Settings.h" + +class SettingsWindow : public QDialog, private Ui::DialogSettings +{ + Q_OBJECT +public: + SettingsWindow(QWidget *parent = nullptr); + ~SettingsWindow(); +public slots: + +private: + +}; diff --git a/apps/browser/Types.hpp b/apps/browser/Types.hpp new file mode 100644 index 0000000..d934b01 --- /dev/null +++ b/apps/browser/Types.hpp @@ -0,0 +1,11 @@ +// +// Created by koncord on 07.05.17. +// + +#pragma once + +#include +#include + +typedef QPair AddrPair; +typedef QPair ServerRow; diff --git a/apps/browser/Version.hpp b/apps/browser/Version.hpp new file mode 100644 index 0000000..1f504a4 --- /dev/null +++ b/apps/browser/Version.hpp @@ -0,0 +1,16 @@ +// +// Created by koncord on 07.08.18. +// + +#pragma once + +#include + +#define MAJOR_VERSION 1 +#define MINOR_VERSION 0 + +unsigned long constexpr make_version(unsigned long major, unsigned long minor) { return (major << 32) + minor; } +unsigned long constexpr version() { return make_version(MAJOR_VERSION, MINOR_VERSION); } +inline std::string strVersion() noexcept { return std::to_string(MAJOR_VERSION) + "." + std::to_string(MINOR_VERSION); } + +#define FIX_UNTIL(_version, message) static_assert(version() < _version, message) diff --git a/apps/browser/main.cpp b/apps/browser/main.cpp new file mode 100644 index 0000000..7fac20d --- /dev/null +++ b/apps/browser/main.cpp @@ -0,0 +1,21 @@ +#include +#include +#include +#include +#include "MainWindow.hpp" +#include + +int main(int argc, char *argv[]) +{ + qRegisterMetaType("Server"); + qRegisterMetaType("ServerExtra"); + + QResource::registerResource("/files/tes3mp/browser.qrc"); + + + QApplication app(argc, argv); + MainWindow d; + + d.show(); + return QApplication::exec(); +} diff --git a/apps/browser/netutils/MasterClient.cpp b/apps/browser/netutils/MasterClient.cpp new file mode 100644 index 0000000..4d20160 --- /dev/null +++ b/apps/browser/netutils/MasterClient.cpp @@ -0,0 +1,270 @@ +// +// Created by koncord on 19.07.18. +// + +#include "MasterClient.hpp" +#include +#include + +#include +#include +#include + +#include + + +MasterClient::MasterClient(QObject *parent) : QObject(parent) +{ + //thread = new QThread; + worker = new SocketWorker; + + connect(worker, &SocketWorker::parsedServer, [this](Server server) { + emit this->server(std::move(server)); + }); + + connect(worker, &SocketWorker::finished, this, &MasterClient::finished); + + + connect(worker, &SocketWorker::serverExtra, [this](Server server, ServerExtra extra) { + emit this->singleServer(std::move(server), std::move(extra)); + }); + + /*connect(thread, &QThread::finished, [this]() { + //worker->deleteLater(); + qDebug() << "thread finished"; + });*/ + + //connect(worker, &SocketWorker::finished, thread, &QThread::quit); + + //worker->moveToThread(thread); + + nam = new QNetworkAccessManager(this); +} + +MasterClient::~MasterClient() +{ + +} + +MasterClient *MasterClient::mThis = nullptr; + +void MasterClient::create(QObject *parent) +{ + if (!mThis) + mThis = new MasterClient(parent); +} + +MasterClient *MasterClient::get() +{ + return mThis; +} + +void MasterClient::address(const QString &address) +{ + addr = address; + worker->address(address); +} + +void MasterClient::error(QAbstractSocket::SocketError socketError) +{ + qDebug() << "err: " << socketError; +} + +void MasterClient::requestList() +{ + /*if (thread) + { + thread->quit(); + thread->wait(); + } + + disconnect(thread, &QThread::started, nullptr, nullptr); + + connect(thread, &QThread::started, [this]() { + worker->requestList(); + worker->process(); + }); + + thread->start();*/ + + worker->requestList(); + worker->process(); +} + +void MasterClient::requestServer(const Server &server) +{ + + /*thread->quit(); + thread->wait(); + + + disconnect(thread, &QThread::started, nullptr, nullptr); + + connect(thread, &QThread::started, [this, server]() { + worker->requestExtra(server); + worker->process(); + }); + + thread->start();*/ + + worker->requestExtra(server); + worker->process(); +} + + +void MasterClient::requestLatestVersionStr() +{ + nam->get(QNetworkRequest(QUrl("http://" + addr + "/browser/version"))); + connect(nam, &QNetworkAccessManager::finished, + [this](QNetworkReply *reply) { + QString version = reply->readAll(); + emit latestVersion(version); + disconnect(nam, &QNetworkAccessManager::finished, nullptr, nullptr); + }); +} + +SocketWorker::SocketWorker() : webSocket(nullptr) +{ +} + +SocketWorker::~SocketWorker() +{ + qDebug() << "SocketWorker::~SocketWorker()"; +} + +void SocketWorker::address(const QString &_addr) +{ + addr = "ws://" + _addr + "/websocket"; +} + +void SocketWorker::requestExtra(const Server &server) +{ + serverId = QString::fromStdString(server.id); +} + +void SocketWorker::requestList() +{ + serverId.clear(); +} + +bool OnRequestError(nlohmann::json &json) +{ + if (json.count("status") == 1) + { + auto status = QString::fromStdString(json["status"]); + auto msg = QString::fromStdString(json["message"]); + QMessageBox::critical(nullptr, status, msg); + return true; + } + return false; +} + +Server getBasic(const nlohmann::json &json, bool extra = false) +{ + Server server; + server.modname = json["modname"]; + server.hostname = json["hostname"]; + server.version = json["version"]; + + if (server.version.empty()) + server.version = "Unknown"; + + server.maxPlayers = json["maxPlayers"]; + server.port = json["port"]; + server.address = json["address"]; + server.password = json["passw"]; + if (!extra) + { + server.id = json["id"]; + server.players = json["players"]; + } + + return server; +} + +void SocketWorker::process() +{ + if(!webSocket) + { + webSocket = new QWebSocket; + //connect(webSocket, &QWebSocket::disconnected, this, &SocketWorker::finished); + connect(webSocket, &QWebSocket::disconnected, [this](){ + qDebug() << "Disconnected" << webSocket->closeReason(); + }); + + connect(webSocket, &QWebSocket::binaryMessageReceived, [this](const QByteArray &message) { + if (serverId.isEmpty()) + return; + + qDebug() << "binaryMessageReceived"; + + nlohmann::json json = nlohmann::json::from_cbor(message.data()); + + if (OnRequestError(json) || json.empty()) + return; + + Server server = getBasic(json, true); + ServerExtra extra; + + extra.dlServer = json["dlServer"]; + extra.dlServer = json["dlServer"]; + + for (const auto &player : json["players"]) + extra.players.emplace_back(player); + + for (const auto &plugin : json["plugins"]) + extra.plugins.emplace_back(plugin); + + for (const auto &item : json["extraInfo"].items()) + extra.extraInfo[item.key()] = item.value(); + + emit serverExtra(std::move(server), std::move(extra)); + }); + + connect(webSocket, &QWebSocket::binaryFrameReceived, [this](const QByteArray &frame, bool last) { + if (!serverId.isEmpty()) + return; + nlohmann::json json = nlohmann::json::from_cbor(frame.data(), frame.size()); + + if (!(OnRequestError(json) || json.empty())) + { + Server server = getBasic(json); + emit parsedServer(std::move(server)); + } + + if(last) + emit finished(); // emit finished if frame is last + }); + } + + auto fnSend = [this]() { + qint64 bytesSent; + + if (serverId.isEmpty()) + { + qDebug() << "GET SERVERS"; + bytesSent = webSocket->sendTextMessage("GET SERVERS"); + } + else + { + qDebug() << "GET SERVER " + serverId; + bytesSent = webSocket->sendTextMessage("GET SERVER " + serverId); + } + if (bytesSent == 0) + qDebug() << "Not connected"; + return bytesSent; + }; + + connect(webSocket, &QWebSocket::connected, [this, fnSend]() { + qint64 bytesSent = fnSend(); + qDebug() << "Bytes sent:" << bytesSent; + }); + + if (fnSend() == 0) + webSocket->open(addr); // we are not connected +} + +void SocketWorker::abort() +{ + webSocket->close(); +} diff --git a/apps/browser/netutils/MasterClient.hpp b/apps/browser/netutils/MasterClient.hpp new file mode 100644 index 0000000..fda9051 --- /dev/null +++ b/apps/browser/netutils/MasterClient.hpp @@ -0,0 +1,77 @@ +// +// Created by koncord on 19.07.18. +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +class SocketWorker; + +class MasterClient: public QObject +{ + Q_OBJECT +public: + static void create(QObject *parent); + static MasterClient *get(); + + void address(const QString &addr); + + void requestList(); + void requestServer(const Server &server); + void requestLatestVersionStr(); + + ~MasterClient(); +protected: + explicit MasterClient(QObject *parent); + + QString addr; + QThread *thread; + SocketWorker *worker; + QNetworkAccessManager *nam; +private slots: + void error(QAbstractSocket::SocketError); + +signals: + void server(Server server); + void finished(); + + void singleServer(Server server, ServerExtra extra); + void latestVersion(const QString &version); +private: + static MasterClient *mThis; +}; + +class SocketWorker : public QObject +{ +Q_OBJECT +public: + SocketWorker(); + ~SocketWorker(); + void requestExtra(const Server &server); + void requestList(); + void address(const QString &addr); +public slots: + void process(); + void abort(); +signals: + void parsedServer(Server server); + void serverExtra(Server server, ServerExtra extra); + void finished(); +private: + QUrl addr; + QWebSocket *webSocket; + QString serverId; +}; diff --git a/apps/browser/netutils/Utils.cpp b/apps/browser/netutils/Utils.cpp new file mode 100644 index 0000000..f9e007e --- /dev/null +++ b/apps/browser/netutils/Utils.cpp @@ -0,0 +1,61 @@ +// +// Created by koncord on 07.01.17. +// + +#include +#include +#include +#include + +#include + +#include "Utils.hpp" + +using namespace std; + +unsigned int PingRakNetServer(const char *addr, unsigned short port) +{ + bool done = false; + RakNet::TimeMS time = PING_UNREACHABLE; + + RakNet::SocketDescriptor socketDescriptor{0, ""}; + RakNet::RakPeerInterface *peer = RakNet::RakPeerInterface::GetInstance(); + peer->Startup(1, &socketDescriptor, 1); + if (!peer->Ping(addr, port, false)) + return time; + RakNet::TimeMS start = RakNet::GetTimeMS(); + while (!done) + { + RakNet::TimeMS now = RakNet::GetTimeMS(); + if (now - start >= PING_UNREACHABLE) + break; + + RakNet::Packet *packet = peer->Receive(); + if (!packet) + continue; + + switch (packet->data[0]) + { + case ID_DISCONNECTION_NOTIFICATION: + case ID_CONNECTION_LOST: + done = true; + break; + case ID_CONNECTED_PING: + case ID_UNCONNECTED_PONG: + { + RakNet::BitStream bsIn(&packet->data[1], packet->length, false); + bsIn.Read(time); + time = now - time; + done = true; + break; + } + default: + break; + } + peer->DeallocatePacket(packet); + } + + peer->Shutdown(0); + RakNet::RakPeerInterface::DestroyInstance(peer); + return time > PING_UNREACHABLE ? PING_UNREACHABLE : time; +} diff --git a/apps/browser/netutils/Utils.hpp b/apps/browser/netutils/Utils.hpp new file mode 100644 index 0000000..5c4964f --- /dev/null +++ b/apps/browser/netutils/Utils.hpp @@ -0,0 +1,13 @@ +// +// Created by koncord on 07.01.17. +// + +#pragma once + +#include +#include + + +#define PING_UNREACHABLE 999 + +unsigned int PingRakNetServer(const char *addr, unsigned short port); diff --git a/cmake/FindRakNet.cmake b/cmake/FindRakNet.cmake new file mode 100644 index 0000000..7fb8fca --- /dev/null +++ b/cmake/FindRakNet.cmake @@ -0,0 +1,75 @@ +# Comes form project edunetgames +# - Try to find RakNet +# Once done this will define +# +# RakNet_FOUND - system has RakNet +# RakNet_INCLUDES - the RakNet include directory +# RakNet_LIBRARY - Link these to use RakNet + + +FIND_LIBRARY (RakNet_LIBRARY_RELEASE NAMES RakNetLibStatic + PATHS + ENV LD_LIBRARY_PATH + ENV LIBRARY_PATH + /usr/lib64 + /usr/lib + /usr/local/lib64 + /usr/local/lib + /opt/local/lib + $ENV{RAKNET_ROOT}/lib + ) + +FIND_LIBRARY (RakNet_LIBRARY_DEBUG NAMES RakNetLibStatic + PATHS + ENV LD_LIBRARY_PATH + ENV LIBRARY_PATH + /usr/lib64 + /usr/lib + /usr/local/lib64 + /usr/local/lib + /opt/local/lib + $ENV{RAKNET_ROOT}/lib + ) + +FIND_PATH (RakNet_INCLUDES raknet/RakPeer.h + ENV CPATH + /usr/include + /usr/local/include + /opt/local/include + $ENV{RAKNET_ROOT}/include + ) + +MESSAGE(STATUS ${RakNet_INCLUDES}) +MESSAGE(STATUS ${RakNet_LIBRARY_RELEASE}) + +IF(RakNet_INCLUDES AND RakNet_LIBRARY_RELEASE) + SET(RakNet_FOUND TRUE) +ENDIF(RakNet_INCLUDES AND RakNet_LIBRARY_RELEASE) + +IF(RakNet_FOUND) + SET(RakNet_INCLUDES ${RakNet_INCLUDES}/raknet) + + + IF (CMAKE_CONFIGURATION_TYPES OR CMAKE_BUILD_TYPE) + SET(RakNet_LIBRARY optimized ${RakNet_LIBRARY_RELEASE} debug ${RakNet_LIBRARY_DEBUG}) + IF(WIN32) + SET(RakNet_LIBRARY optimized ${RakNet_LIBRARY_RELEASE} debug ${RakNet_LIBRARY_DEBUG} ws2_32.lib) + ENDIF(WIN32) + ELSE() + # if there are no configuration types and CMAKE_BUILD_TYPE has no value + # then just use the release libraries + SET(RakNet_LIBRARY ${RakNet_LIBRARY_RELEASE} ) + IF(WIN32) + SET(RakNet_LIBRARY ${RakNet_LIBRARY_RELEASE} ws2_32.lib) + ENDIF(WIN32) + ENDIF() + + IF(NOT RakNet_FIND_QUIETLY) + MESSAGE(STATUS "Found RakNet_LIBRARY_RELEASE: ${RakNet_LIBRARY_RELEASE}") + MESSAGE(STATUS "Found RakNet_INCLUDES: ${RakNet_INCLUDES}") + ENDIF(NOT RakNet_FIND_QUIETLY) +ELSE(RakNet_FOUND) + IF(RakNet_FIND_REQUIRED) + MESSAGE(FATAL_ERROR "Could not find RakNet") + ENDIF(RakNet_FIND_REQUIRED) +ENDIF(RakNet_FOUND) \ No newline at end of file diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt new file mode 100644 index 0000000..7f54b9d --- /dev/null +++ b/components/CMakeLists.txt @@ -0,0 +1,36 @@ +IF(NOT WIN32 AND NOT APPLE) + add_definitions(-DGLOBAL_DATA_PATH="${GLOBAL_DATA_PATH}") + add_definitions(-DGLOBAL_CONFIG_PATH="${GLOBAL_CONFIG_PATH}") +ENDIF() + +set(COMPONENT_FILES + process/processinvoker.cpp + settings/settings.cpp + + files/collections.cpp + files/configurationmanager.cpp + files/escape.cpp + files/linuxpath.cpp + files/multidircollection.cpp + ) + +set(COMPONENT_MOC_FILES + process/processinvoker.hpp + ) + +QT5_WRAP_CPP(MOC_SRCS ${COMPONENT_MOC_FILES}) + +add_library(components STATIC ${COMPONENT_FILES} ${MOC_SRCS}) + +target_link_libraries(components Qt5::Widgets Qt5::Core) + +target_link_libraries(components + ${Boost_SYSTEM_LIBRARY} + ${Boost_FILESYSTEM_LIBRARY} + ${Boost_PROGRAM_OPTIONS_LIBRARY} + ) + +# Fix for not visible pthreads functions for linker with glibc 2.15 +if (UNIX AND NOT APPLE) + target_link_libraries(components ${CMAKE_THREAD_LIBS_INIT}) +endif() diff --git a/components/files/collections.cpp b/components/files/collections.cpp new file mode 100644 index 0000000..a933eb6 --- /dev/null +++ b/components/files/collections.cpp @@ -0,0 +1,85 @@ +#include "collections.hpp" + +#include + +namespace Files +{ + Collections::Collections() + : mDirectories() + , mFoldCase(false) + , mCollections() + { + } + + Collections::Collections(const Files::PathContainer& directories, bool foldCase) + : mDirectories(directories) + , mFoldCase(foldCase) + , mCollections() + { + } + + const MultiDirCollection& Collections::getCollection(const std::string& extension) const + { + MultiDirCollectionContainer::iterator iter = mCollections.find(extension); + if (iter==mCollections.end()) + { + std::pair result = + mCollections.insert(std::make_pair(extension, MultiDirCollection(mDirectories, extension, mFoldCase))); + + iter = result.first; + } + + return iter->second; + } + + boost::filesystem::path Collections::getPath(const std::string& file) const + { + for (Files::PathContainer::const_iterator iter = mDirectories.begin(); + iter != mDirectories.end(); ++iter) + { + for (boost::filesystem::directory_iterator iter2 (*iter); + iter2!=boost::filesystem::directory_iterator(); ++iter2) + { + boost::filesystem::path path = *iter2; + + if (mFoldCase) + { + if (Misc::StringUtils::ciEqual(file, path.filename().string())) + return path.string(); + } + else if (path.filename().string() == file) + return path.string(); + } + } + + throw std::runtime_error ("file " + file + " not found"); + } + + bool Collections::doesExist(const std::string& file) const + { + for (Files::PathContainer::const_iterator iter = mDirectories.begin(); + iter != mDirectories.end(); ++iter) + { + for (boost::filesystem::directory_iterator iter2 (*iter); + iter2!=boost::filesystem::directory_iterator(); ++iter2) + { + boost::filesystem::path path = *iter2; + + if (mFoldCase) + { + if (Misc::StringUtils::ciEqual(file, path.filename().string())) + return true; + } + else if (path.filename().string() == file) + return true; + } + } + + return false; + } + + const Files::PathContainer& Collections::getPaths() const + { + return mDirectories; + } +} diff --git a/components/files/collections.hpp b/components/files/collections.hpp new file mode 100644 index 0000000..def61cf --- /dev/null +++ b/components/files/collections.hpp @@ -0,0 +1,43 @@ +#ifndef COMPONENTS_FILES_COLLECTION_HPP +#define COMPONENTS_FILES_COLLECTION_HPP + +#include + +#include "multidircollection.hpp" + +namespace Files +{ + class Collections + { + public: + Collections(); + + ///< Directories are listed with increasing priority. + Collections(const Files::PathContainer& directories, bool foldCase); + + ///< Return a file collection for the given extension. Extension must contain the + /// leading dot and must be all lower-case. + const MultiDirCollection& getCollection(const std::string& extension) const; + + boost::filesystem::path getPath(const std::string& file) const; + ///< Return full path (including filename) of \a file. + /// + /// If the file does not exist in any of the collection's + /// directories, an exception is thrown. \a file must include the + /// extension. + + bool doesExist(const std::string& file) const; + ///< \return Does a file with the given name exist? + + const Files::PathContainer& getPaths() const; + + private: + typedef std::map MultiDirCollectionContainer; + Files::PathContainer mDirectories; + + bool mFoldCase; + mutable MultiDirCollectionContainer mCollections; + }; +} + +#endif diff --git a/components/files/configurationmanager.cpp b/components/files/configurationmanager.cpp new file mode 100644 index 0000000..7c3956a --- /dev/null +++ b/components/files/configurationmanager.cpp @@ -0,0 +1,202 @@ +#include "configurationmanager.hpp" + +#include + +#include + +#include +#include +#include +/** + * \namespace Files + */ +namespace Files +{ + +static const char* const openmwCfgFile = "openmw.cfg"; + +#if defined(_WIN32) || defined(__WINDOWS__) +static const char* const applicationName = "OpenMW"; +#else +static const char* const applicationName = "openmw"; +#endif + +const char* const localToken = "?local?"; +const char* const userDataToken = "?userdata?"; +const char* const globalToken = "?global?"; + +ConfigurationManager::ConfigurationManager(bool silent) + : mFixedPath(applicationName) + , mSilent(silent) +{ + setupTokensMapping(); + + boost::filesystem::create_directories(mFixedPath.getUserConfigPath()); + boost::filesystem::create_directories(mFixedPath.getUserDataPath()); + + mLogPath = mFixedPath.getUserConfigPath(); +} + +ConfigurationManager::~ConfigurationManager() +{ +} + +void ConfigurationManager::setupTokensMapping() +{ + mTokensMapping.insert(std::make_pair(localToken, &FixedPath<>::getLocalPath)); + mTokensMapping.insert(std::make_pair(userDataToken, &FixedPath<>::getUserDataPath)); + mTokensMapping.insert(std::make_pair(globalToken, &FixedPath<>::getGlobalDataPath)); +} + +void ConfigurationManager::readConfiguration(boost::program_options::variables_map& variables, + boost::program_options::options_description& description, bool quiet) +{ + bool silent = mSilent; + mSilent = quiet; + + loadConfig(mFixedPath.getUserConfigPath(), variables, description); + boost::program_options::notify(variables); + + // read either local or global config depending on type of installation + bool loaded = loadConfig(mFixedPath.getLocalPath(), variables, description); + boost::program_options::notify(variables); + if (!loaded) + { + loadConfig(mFixedPath.getGlobalConfigPath(), variables, description); + boost::program_options::notify(variables); + } + + mSilent = silent; +} + +void ConfigurationManager::processPaths(Files::PathContainer& dataDirs, bool create) +{ + std::string path; + for (Files::PathContainer::iterator it = dataDirs.begin(); it != dataDirs.end(); ++it) + { + path = it->string(); + + // Check if path contains a token + if (!path.empty() && *path.begin() == '?') + { + std::string::size_type pos = path.find('?', 1); + if (pos != std::string::npos && pos != 0) + { + TokensMappingContainer::iterator tokenIt = mTokensMapping.find(path.substr(0, pos + 1)); + if (tokenIt != mTokensMapping.end()) + { + boost::filesystem::path tempPath(((mFixedPath).*(tokenIt->second))()); + if (pos < path.length() - 1) + { + // There is something after the token, so we should + // append it to the path + tempPath /= path.substr(pos + 1, path.length() - pos); + } + + *it = tempPath; + } + else + { + // Clean invalid / unknown token, it will be removed outside the loop + (*it).clear(); + } + } + } + + if (!boost::filesystem::is_directory(*it)) + { + if (create) + { + try + { + boost::filesystem::create_directories (*it); + } + catch (...) {} + + if (boost::filesystem::is_directory(*it)) + continue; + } + + (*it).clear(); + } + } + + dataDirs.erase(std::remove_if(dataDirs.begin(), dataDirs.end(), + std::bind(&boost::filesystem::path::empty, std::placeholders::_1)), dataDirs.end()); +} + +bool ConfigurationManager::loadConfig(const boost::filesystem::path& path, + boost::program_options::variables_map& variables, + boost::program_options::options_description& description) +{ + boost::filesystem::path cfgFile(path); + cfgFile /= std::string(openmwCfgFile); + if (boost::filesystem::is_regular_file(cfgFile)) + { + if (!mSilent) + std::cout << "Loading config file: " << cfgFile.string() << "... "; + + boost::filesystem::ifstream configFileStreamUnfiltered(cfgFile); + boost::iostreams::filtering_istream configFileStream; + configFileStream.push(escape_hash_filter()); + configFileStream.push(configFileStreamUnfiltered); + if (configFileStreamUnfiltered.is_open()) + { + boost::program_options::store(boost::program_options::parse_config_file( + configFileStream, description, true), variables); + + if (!mSilent) + std::cout << "done." << std::endl; + return true; + } + else + { + if (!mSilent) + std::cout << "failed." << std::endl; + return false; + } + } + return false; +} + +const boost::filesystem::path& ConfigurationManager::getGlobalPath() const +{ + return mFixedPath.getGlobalConfigPath(); +} + +const boost::filesystem::path& ConfigurationManager::getUserConfigPath() const +{ + return mFixedPath.getUserConfigPath(); +} + +const boost::filesystem::path& ConfigurationManager::getUserDataPath() const +{ + return mFixedPath.getUserDataPath(); +} + +const boost::filesystem::path& ConfigurationManager::getLocalPath() const +{ + return mFixedPath.getLocalPath(); +} + +const boost::filesystem::path& ConfigurationManager::getGlobalDataPath() const +{ + return mFixedPath.getGlobalDataPath(); +} + +const boost::filesystem::path& ConfigurationManager::getCachePath() const +{ + return mFixedPath.getCachePath(); +} + +const boost::filesystem::path& ConfigurationManager::getInstallPath() const +{ + return mFixedPath.getInstallPath(); +} + +const boost::filesystem::path& ConfigurationManager::getLogPath() const +{ + return mLogPath; +} + +} /* namespace Cfg */ diff --git a/components/files/configurationmanager.hpp b/components/files/configurationmanager.hpp new file mode 100644 index 0000000..df131e6 --- /dev/null +++ b/components/files/configurationmanager.hpp @@ -0,0 +1,67 @@ +#ifndef COMPONENTS_FILES_CONFIGURATIONMANAGER_HPP +#define COMPONENTS_FILES_CONFIGURATIONMANAGER_HPP + +#include + +#include + +#include +#include + +/** + * \namespace Files + */ +namespace Files +{ + +/** + * \struct ConfigurationManager + */ +struct ConfigurationManager +{ + ConfigurationManager(bool silent=false); /// @param silent Emit log messages to cout? + virtual ~ConfigurationManager(); + + void readConfiguration(boost::program_options::variables_map& variables, + boost::program_options::options_description& description, bool quiet=false); + + void processPaths(Files::PathContainer& dataDirs, bool create = false); + ///< \param create Try creating the directory, if it does not exist. + + /**< Fixed paths */ + const boost::filesystem::path& getGlobalPath() const; + const boost::filesystem::path& getUserConfigPath() const; + const boost::filesystem::path& getLocalPath() const; + + const boost::filesystem::path& getGlobalDataPath() const; + const boost::filesystem::path& getUserDataPath() const; + const boost::filesystem::path& getLocalDataPath() const; + const boost::filesystem::path& getInstallPath() const; + + const boost::filesystem::path& getCachePath() const; + + const boost::filesystem::path& getLogPath() const; + + private: + typedef Files::FixedPath<> FixedPathType; + + typedef const boost::filesystem::path& (FixedPathType::*path_type_f)() const; + typedef std::map TokensMappingContainer; + + bool loadConfig(const boost::filesystem::path& path, + boost::program_options::variables_map& variables, + boost::program_options::options_description& description); + + void setupTokensMapping(); + + FixedPathType mFixedPath; + + boost::filesystem::path mLogPath; + + TokensMappingContainer mTokensMapping; + + bool mSilent; +}; +} /* namespace Cfg */ + +#endif /* COMPONENTS_FILES_CONFIGURATIONMANAGER_HPP */ diff --git a/components/files/escape.cpp b/components/files/escape.cpp new file mode 100644 index 0000000..93ae9b8 --- /dev/null +++ b/components/files/escape.cpp @@ -0,0 +1,145 @@ +#include "escape.hpp" + +#include + +namespace Files +{ + const int escape_hash_filter::sEscape = '@'; + const int escape_hash_filter::sEscapeIdentifier = 'a'; + const int escape_hash_filter::sHashIdentifier = 'h'; + + escape_hash_filter::escape_hash_filter() : mSeenNonWhitespace(false), mFinishLine(false) + { + } + + escape_hash_filter::~escape_hash_filter() + { + } + + unescape_hash_filter::unescape_hash_filter() : expectingIdentifier(false) + { + } + + unescape_hash_filter::~unescape_hash_filter() + { + } + + std::string EscapeHashString::processString(const std::string & str) + { + std::string temp = str; + + static const char hash[] = { escape_hash_filter::sEscape, escape_hash_filter::sHashIdentifier }; + Misc::StringUtils::replaceAll(temp, hash, "#", 2, 1); + + static const char escape[] = { escape_hash_filter::sEscape, escape_hash_filter::sEscapeIdentifier }; + Misc::StringUtils::replaceAll(temp, escape, "@", 2, 1); + + return temp; + } + + EscapeHashString::EscapeHashString() : mData() + { + } + + EscapeHashString::EscapeHashString(const std::string & str) : mData(EscapeHashString::processString(str)) + { + } + + EscapeHashString::EscapeHashString(const std::string & str, size_t pos, size_t len) : mData(EscapeHashString::processString(str), pos, len) + { + } + + EscapeHashString::EscapeHashString(const char * s) : mData(EscapeHashString::processString(std::string(s))) + { + } + + EscapeHashString::EscapeHashString(const char * s, size_t n) : mData(EscapeHashString::processString(std::string(s)), 0, n) + { + } + + EscapeHashString::EscapeHashString(size_t n, char c) : mData(n, c) + { + } + + template + EscapeHashString::EscapeHashString(InputIterator first, InputIterator last) : mData(EscapeHashString::processString(std::string(first, last))) + { + } + + std::string EscapeHashString::toStdString() const + { + return std::string(mData); + } + + std::istream & operator>> (std::istream & is, EscapeHashString & eHS) + { + std::string temp; + is >> temp; + eHS = EscapeHashString(temp); + return is; + } + + std::ostream & operator<< (std::ostream & os, const EscapeHashString & eHS) + { + os << eHS.mData; + return os; + } + + EscapeStringVector::EscapeStringVector() : mVector() + { + } + + EscapeStringVector::~EscapeStringVector() + { + } + + std::vector EscapeStringVector::toStdStringVector() const + { + std::vector temp = std::vector(); + for (std::vector::const_iterator it = mVector.begin(); it != mVector.end(); ++it) + { + temp.push_back(it->toStdString()); + } + return temp; + } + + // boost program options validation + + void validate(boost::any &v, const std::vector &tokens, Files::EscapeHashString * eHS, int a) + { + boost::program_options::validators::check_first_occurrence(v); + + if (v.empty()) + v = boost::any(EscapeHashString(boost::program_options::validators::get_single_string(tokens))); + } + + void validate(boost::any &v, const std::vector &tokens, EscapeStringVector *, int) + { + if (v.empty()) + v = boost::any(EscapeStringVector()); + + EscapeStringVector * eSV = boost::any_cast(&v); + + for (std::vector::const_iterator it = tokens.begin(); it != tokens.end(); ++it) + eSV->mVector.push_back(EscapeHashString(*it)); + } + + PathContainer EscapePath::toPathContainer(const EscapePathContainer & escapePathContainer) + { + PathContainer temp; + for (EscapePathContainer::const_iterator it = escapePathContainer.begin(); it != escapePathContainer.end(); ++it) + temp.push_back(it->mPath); + return temp; + } + + std::istream & operator>> (std::istream & istream, EscapePath & escapePath) + { + boost::iostreams::filtering_istream filteredStream; + filteredStream.push(unescape_hash_filter()); + filteredStream.push(istream); + + filteredStream >> escapePath.mPath; + + return istream; + } +} diff --git a/components/files/escape.hpp b/components/files/escape.hpp new file mode 100644 index 0000000..d01bd8d --- /dev/null +++ b/components/files/escape.hpp @@ -0,0 +1,191 @@ +#ifndef COMPONENTS_FILES_ESCAPE_HPP +#define COMPONENTS_FILES_ESCAPE_HPP + +#include + +#include + +#include +#include +#include + +/** + * \namespace Files + */ +namespace Files +{ + /** + * \struct escape_hash_filter + */ + struct escape_hash_filter : public boost::iostreams::input_filter + { + static const int sEscape; + static const int sHashIdentifier; + static const int sEscapeIdentifier; + + escape_hash_filter(); + virtual ~escape_hash_filter(); + + template int get(Source & src); + + private: + std::queue mNext; + + bool mSeenNonWhitespace; + bool mFinishLine; + }; + + template + int escape_hash_filter::get(Source & src) + { + if (mNext.empty()) + { + int character = boost::iostreams::get(src); + if (character == boost::iostreams::WOULD_BLOCK) + { + mNext.push(character); + } + else if (character == EOF) + { + mSeenNonWhitespace = false; + mFinishLine = false; + mNext.push(character); + } + else if (character == '\n') + { + mSeenNonWhitespace = false; + mFinishLine = false; + mNext.push(character); + } + else if (mFinishLine) + { + mNext.push(character); + } + else if (character == '#') + { + if (mSeenNonWhitespace) + { + mNext.push(sEscape); + mNext.push(sHashIdentifier); + } + else + { + //it's fine being interpreted by Boost as a comment, and so is anything afterwards + mNext.push(character); + mFinishLine = true; + } + } + else if (character == sEscape) + { + mNext.push(sEscape); + mNext.push(sEscapeIdentifier); + } + else + { + mNext.push(character); + } + if (!mSeenNonWhitespace && !isspace(character)) + mSeenNonWhitespace = true; + } + int retval = mNext.front(); + mNext.pop(); + return retval; + } + + struct unescape_hash_filter : public boost::iostreams::input_filter + { + unescape_hash_filter(); + virtual ~unescape_hash_filter(); + + template int get(Source & src); + + private: + bool expectingIdentifier; + }; + + template + int unescape_hash_filter::get(Source & src) + { + int character; + if (!expectingIdentifier) + character = boost::iostreams::get(src); + else + { + character = escape_hash_filter::sEscape; + expectingIdentifier = false; + } + if (character == escape_hash_filter::sEscape) + { + int nextChar = boost::iostreams::get(src); + int intended; + if (nextChar == escape_hash_filter::sEscapeIdentifier) + intended = escape_hash_filter::sEscape; + else if (nextChar == escape_hash_filter::sHashIdentifier) + intended = '#'; + else if (nextChar == boost::iostreams::WOULD_BLOCK) + { + expectingIdentifier = true; + intended = nextChar; + } + else + intended = '?'; + return intended; + } + else + return character; + } + + /** + * \class EscapeHashString + */ + class EscapeHashString + { + private: + std::string mData; + public: + static std::string processString(const std::string & str); + + EscapeHashString(); + EscapeHashString(const std::string & str); + EscapeHashString(const std::string & str, size_t pos, size_t len = std::string::npos); + EscapeHashString(const char * s); + EscapeHashString(const char * s, size_t n); + EscapeHashString(size_t n, char c); + template + EscapeHashString(InputIterator first, InputIterator last); + + std::string toStdString() const; + + friend std::ostream & operator<< (std::ostream & os, const EscapeHashString & eHS); + }; + + std::istream & operator>> (std::istream & is, EscapeHashString & eHS); + + struct EscapeStringVector + { + std::vector mVector; + + EscapeStringVector(); + virtual ~EscapeStringVector(); + + std::vector toStdStringVector() const; + }; + + //boost program options validation + + void validate(boost::any &v, const std::vector &tokens, Files::EscapeHashString * eHS, int a); + + void validate(boost::any &v, const std::vector &tokens, EscapeStringVector *, int); + + struct EscapePath + { + boost::filesystem::path mPath; + + static PathContainer toPathContainer(const std::vector & escapePathContainer); + }; + + typedef std::vector EscapePathContainer; + + std::istream & operator>> (std::istream & istream, EscapePath & escapePath); +} /* namespace Files */ +#endif /* COMPONENTS_FILES_ESCAPE_HPP */ diff --git a/components/files/fixedpath.hpp b/components/files/fixedpath.hpp new file mode 100644 index 0000000..2e72b81 --- /dev/null +++ b/components/files/fixedpath.hpp @@ -0,0 +1,129 @@ +#ifndef COMPONENTS_FILES_FIXEDPATH_HPP +#define COMPONENTS_FILES_FIXEDPATH_HPP + +#include +#include + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(__OpenBSD__) +#ifndef ANDROID + #include + namespace Files { typedef LinuxPath TargetPathType; } +#else + #include + namespace Files { typedef AndroidPath TargetPathType; } +#endif +#elif defined(__WIN32) || defined(__WINDOWS__) || defined(_WIN32) + #include + namespace Files { typedef WindowsPath TargetPathType; } + +#elif defined(macintosh) || defined(Macintosh) || defined(__APPLE__) || defined(__MACH__) + #include + namespace Files { typedef MacOsPath TargetPathType; } + +#else + #error "Unknown platform!" +#endif + + +/** + * \namespace Files + */ +namespace Files +{ + +/** + * \struct Path + * + * \tparam P - Path strategy class type (depends on target system) + * + */ +template +< + class P = TargetPathType +> +struct FixedPath +{ + typedef P PathType; + + /** + * \brief Path constructor. + * + * \param [in] application_name - Name of the application + */ + FixedPath(const std::string& application_name) + : mPath(application_name + "/") + , mUserConfigPath(mPath.getUserConfigPath()) + , mUserDataPath(mPath.getUserDataPath()) + , mGlobalConfigPath(mPath.getGlobalConfigPath()) + , mLocalPath(mPath.getLocalPath()) + , mGlobalDataPath(mPath.getGlobalDataPath()) + , mCachePath(mPath.getCachePath()) + , mInstallPath(mPath.getInstallPath()) + { + } + + /** + * \brief Return path pointing to the user local configuration directory. + */ + const boost::filesystem::path& getUserConfigPath() const + { + return mUserConfigPath; + } + + const boost::filesystem::path& getUserDataPath() const + { + return mUserDataPath; + } + + /** + * \brief Return path pointing to the global (system) configuration directory. + */ + const boost::filesystem::path& getGlobalConfigPath() const + { + return mGlobalConfigPath; + } + + /** + * \brief Return path pointing to the directory where application was started. + */ + const boost::filesystem::path& getLocalPath() const + { + return mLocalPath; + } + + + const boost::filesystem::path& getInstallPath() const + { + return mInstallPath; + } + + const boost::filesystem::path& getGlobalDataPath() const + { + return mGlobalDataPath; + } + + const boost::filesystem::path& getCachePath() const + { + return mCachePath; + } + + private: + PathType mPath; + + boost::filesystem::path mUserConfigPath; /**< User path */ + boost::filesystem::path mUserDataPath; + boost::filesystem::path mGlobalConfigPath; /**< Global path */ + boost::filesystem::path mLocalPath; /**< It is the same directory where application was run */ + + boost::filesystem::path mGlobalDataPath; /**< Global application data path */ + + boost::filesystem::path mCachePath; + + boost::filesystem::path mInstallPath; + +}; + + +} /* namespace Files */ + +#endif /* COMPONENTS_FILES_FIXEDPATH_HPP */ diff --git a/components/files/linuxpath.cpp b/components/files/linuxpath.cpp new file mode 100644 index 0000000..1f6a3d9 --- /dev/null +++ b/components/files/linuxpath.cpp @@ -0,0 +1,160 @@ +#include "linuxpath.hpp" + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(__OpenBSD__) + +#include +#include +#include + +#include + + +namespace +{ + boost::filesystem::path getUserHome() + { + const char* dir = getenv("HOME"); + if (dir == NULL) + { + struct passwd* pwd = getpwuid(getuid()); + if (pwd != NULL) + { + dir = pwd->pw_dir; + } + } + if (dir == NULL) + return boost::filesystem::path(); + else + return boost::filesystem::path(dir); + } + + boost::filesystem::path getEnv(const std::string& envVariable, const boost::filesystem::path& fallback) + { + const char* result = getenv(envVariable.c_str()); + if (!result) + return fallback; + boost::filesystem::path dir(result); + if (dir.empty()) + return fallback; + else + return dir; + } +} + +/** + * \namespace Files + */ +namespace Files +{ + +LinuxPath::LinuxPath(const std::string& application_name) + : mName(application_name) +{ +} + +boost::filesystem::path LinuxPath::getUserConfigPath() const +{ + return getEnv("XDG_CONFIG_HOME", getUserHome() / ".config") / mName; +} + +boost::filesystem::path LinuxPath::getUserDataPath() const +{ + return getEnv("XDG_DATA_HOME", getUserHome() / ".local/share") / mName; +} + +boost::filesystem::path LinuxPath::getCachePath() const +{ + return getEnv("XDG_CACHE_HOME", getUserHome() / ".cache") / mName; +} + +boost::filesystem::path LinuxPath::getGlobalConfigPath() const +{ + boost::filesystem::path globalPath(GLOBAL_CONFIG_PATH); + return globalPath / mName; +} + +boost::filesystem::path LinuxPath::getLocalPath() const +{ + return boost::filesystem::path("./"); +} + +boost::filesystem::path LinuxPath::getGlobalDataPath() const +{ + boost::filesystem::path globalDataPath(GLOBAL_DATA_PATH); + return globalDataPath / mName; +} + +boost::filesystem::path LinuxPath::getInstallPath() const +{ + boost::filesystem::path installPath; + + boost::filesystem::path homePath = getUserHome(); + + if (!homePath.empty()) + { + boost::filesystem::path wineDefaultRegistry(homePath); + wineDefaultRegistry /= ".wine/system.reg"; + + if (boost::filesystem::is_regular_file(wineDefaultRegistry)) + { + boost::filesystem::ifstream file(wineDefaultRegistry); + bool isRegEntry = false; + std::string line; + std::string mwpath; + + while (std::getline(file, line)) + { + if (line[0] == '[') // we found an entry + { + if (isRegEntry) + { + break; + } + + isRegEntry = (line.find("Softworks\\\\Morrowind]") != std::string::npos); + } + else if (isRegEntry) + { + if (line[0] == '"') // empty line means new registry key + { + std::string key = line.substr(1, line.find('"', 1) - 1); + if (strcasecmp(key.c_str(), "Installed Path") == 0) + { + std::string::size_type valuePos = line.find('=') + 2; + mwpath = line.substr(valuePos, line.rfind('"') - valuePos); + + std::string::size_type pos = mwpath.find("\\"); + while (pos != std::string::npos) + { + mwpath.replace(pos, 2, "/"); + pos = mwpath.find("\\", pos + 1); + } + break; + } + } + } + } + + if (!mwpath.empty()) + { + // Change drive letter to lowercase, so we could use + // ~/.wine/dosdevices symlinks + mwpath[0] = Misc::StringUtils::toLower(mwpath[0]); + installPath /= homePath; + installPath /= ".wine/dosdevices/"; + installPath /= mwpath; + + if (!boost::filesystem::is_directory(installPath)) + { + installPath.clear(); + } + } + } + } + + return installPath; +} + +} /* namespace Files */ + +#endif /* defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(__OpenBSD__) */ diff --git a/components/files/linuxpath.hpp b/components/files/linuxpath.hpp new file mode 100644 index 0000000..7950157 --- /dev/null +++ b/components/files/linuxpath.hpp @@ -0,0 +1,61 @@ +#ifndef COMPONENTS_FILES_LINUXPATH_H +#define COMPONENTS_FILES_LINUXPATH_H + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(__OpenBSD__) + +#include + +/** + * \namespace Files + */ +namespace Files +{ + +/** + * \struct LinuxPath + */ +struct LinuxPath +{ + LinuxPath(const std::string& application_name); + + /** + * \brief Return path to the user directory. + */ + boost::filesystem::path getUserConfigPath() const; + + boost::filesystem::path getUserDataPath() const; + + /** + * \brief Return path to the global (system) directory where config files can be placed. + */ + boost::filesystem::path getGlobalConfigPath() const; + + /** + * \brief Return path to the runtime configuration directory which is the + * place where an application was started. + */ + boost::filesystem::path getLocalPath() const; + + /** + * \brief Return path to the global (system) directory where game files can be placed. + */ + boost::filesystem::path getGlobalDataPath() const; + + /** + * \brief + */ + boost::filesystem::path getCachePath() const; + + /** + * \brief Gets the path of the installed Morrowind version if there is one. + */ + boost::filesystem::path getInstallPath() const; + + std::string mName; +}; + +} /* namespace Files */ + +#endif /* defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(__OpenBSD__) */ + +#endif /* COMPONENTS_FILES_LINUXPATH_H */ diff --git a/components/files/multidircollection.cpp b/components/files/multidircollection.cpp new file mode 100644 index 0000000..93db683 --- /dev/null +++ b/components/files/multidircollection.cpp @@ -0,0 +1,107 @@ +#include "multidircollection.hpp" + +#include + +#include + +namespace Files +{ + struct NameEqual + { + bool mStrict; + + NameEqual (bool strict) : mStrict (strict) {} + + bool operator() (const std::string& left, const std::string& right) const + { + if (mStrict) + return left==right; + + std::size_t len = left.length(); + + if (len!=right.length()) + return false; + + for (std::size_t i=0; ifirst==filename) + { + mFiles[filename] = path; + } + else + { + // handle case folding + mFiles.erase (result->first); + mFiles.insert (std::make_pair (filename, path)); + } + } + } + } + + boost::filesystem::path MultiDirCollection::getPath (const std::string& file) const + { + TIter iter = mFiles.find (file); + + if (iter==mFiles.end()) + throw std::runtime_error ("file " + file + " not found"); + + return iter->second; + } + + bool MultiDirCollection::doesExist (const std::string& file) const + { + return mFiles.find (file)!=mFiles.end(); + } + + MultiDirCollection::TIter MultiDirCollection::begin() const + { + return mFiles.begin(); + } + + MultiDirCollection::TIter MultiDirCollection::end() const + { + return mFiles.end(); + } +} diff --git a/components/files/multidircollection.hpp b/components/files/multidircollection.hpp new file mode 100644 index 0000000..c213c8f --- /dev/null +++ b/components/files/multidircollection.hpp @@ -0,0 +1,88 @@ +#ifndef COMPONENTS_FILES_MULTIDIRSOLLECTION_HPP +#define COMPONENTS_FILES_MULTIDIRSOLLECTION_HPP + +#include +#include +#include +#include + +#include + +#include + +namespace Files +{ + typedef std::vector PathContainer; + + struct NameLess + { + bool mStrict; + + NameLess (bool strict) : mStrict (strict) {} + + bool operator() (const std::string& left, const std::string& right) const + { + if (mStrict) + return leftr) + return false; + } + + return left.length() TContainer; + typedef TContainer::const_iterator TIter; + + private: + + TContainer mFiles; + + public: + + MultiDirCollection (const Files::PathContainer& directories, + const std::string& extension, bool foldCase); + ///< Directories are listed with increasing priority. + /// \param extension The extension that should be listed in this collection. Must + /// contain the leading dot. + /// \param foldCase Ignore filename case + + boost::filesystem::path getPath (const std::string& file) const; + ///< Return full path (including filename) of \a file. + /// + /// If the file does not exist, an exception is thrown. \a file must include + /// the extension. + + bool doesExist (const std::string& file) const; + ///< \return Does a file with the given name exist? + + TIter begin() const; + ///< Return iterator pointing to the first file. + + TIter end() const; + ///< Return iterator pointing past the last file. + + }; +} + +#endif diff --git a/components/misc/stringops.hpp b/components/misc/stringops.hpp new file mode 100644 index 0000000..0fde1c9 --- /dev/null +++ b/components/misc/stringops.hpp @@ -0,0 +1,241 @@ +#ifndef MISC_STRINGOPS_H +#define MISC_STRINGOPS_H + +#include +#include +#include +#include + +#include "utf8stream.hpp" + +namespace Misc +{ +class StringUtils +{ + struct ci + { + bool operator()(char x, char y) const { + return toLower(x) < toLower(y); + } + }; + +public: + + /// Plain and simple locale-unaware toLower. Anything from A to Z is lower-cased, multibyte characters are unchanged. + /// Don't use std::tolower(char, locale&) because that is abysmally slow. + /// Don't use tolower(int) because that depends on global locale. + static char toLower(char c) + { + switch(c) + { + case 'A':return 'a'; + case 'B':return 'b'; + case 'C':return 'c'; + case 'D':return 'd'; + case 'E':return 'e'; + case 'F':return 'f'; + case 'G':return 'g'; + case 'H':return 'h'; + case 'I':return 'i'; + case 'J':return 'j'; + case 'K':return 'k'; + case 'L':return 'l'; + case 'M':return 'm'; + case 'N':return 'n'; + case 'O':return 'o'; + case 'P':return 'p'; + case 'Q':return 'q'; + case 'R':return 'r'; + case 'S':return 's'; + case 'T':return 't'; + case 'U':return 'u'; + case 'V':return 'v'; + case 'W':return 'w'; + case 'X':return 'x'; + case 'Y':return 'y'; + case 'Z':return 'z'; + default:return c; + }; + } + + static Utf8Stream::UnicodeChar toLowerUtf8(Utf8Stream::UnicodeChar ch) + { + // Russian alphabet + if (ch >= 0x0410 && ch < 0x0430) + return ch += 0x20; + + // Cyrillic IO character + if (ch == 0x0401) + return ch += 0x50; + + // Latin alphabet + if (ch >= 0x41 && ch < 0x60) + return ch += 0x20; + + // Deutch characters + if (ch == 0xc4 || ch == 0xd6 || ch == 0xdc) + return ch += 0x20; + if (ch == 0x1e9e) + return 0xdf; + + // TODO: probably we will need to support characters from other languages + + return ch; + } + + static std::string lowerCaseUtf8(const std::string str) + { + if (str.empty()) + return str; + + // Decode string as utf8 characters, convert to lower case and pack them to string + std::string out; + Utf8Stream stream (str.c_str()); + while (!stream.eof ()) + { + Utf8Stream::UnicodeChar character = toLowerUtf8(stream.peek()); + + if (character <= 0x7f) + out.append(1, static_cast(character)); + else if (character <= 0x7ff) + { + out.append(1, static_cast(0xc0 | ((character >> 6) & 0x1f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + else if (character <= 0xffff) + { + out.append(1, static_cast(0xe0 | ((character >> 12) & 0x0f))); + out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + else + { + out.append(1, static_cast(0xf0 | ((character >> 18) & 0x07))); + out.append(1, static_cast(0x80 | ((character >> 12) & 0x3f))); + out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + + stream.consume(); + } + + return out; + } + + static bool ciLess(const std::string &x, const std::string &y) { + return std::lexicographical_compare(x.begin(), x.end(), y.begin(), y.end(), ci()); + } + + static bool ciEqual(const std::string &x, const std::string &y) { + if (x.size() != y.size()) { + return false; + } + std::string::const_iterator xit = x.begin(); + std::string::const_iterator yit = y.begin(); + for (; xit != x.end(); ++xit, ++yit) { + if (toLower(*xit) != toLower(*yit)) { + return false; + } + } + return true; + } + + static int ciCompareLen(const std::string &x, const std::string &y, size_t len) + { + std::string::const_iterator xit = x.begin(); + std::string::const_iterator yit = y.begin(); + for(;xit != x.end() && yit != y.end() && len > 0;++xit,++yit,--len) + { + int res = *xit - *yit; + if(res != 0 && toLower(*xit) != toLower(*yit)) + return (res > 0) ? 1 : -1; + } + if(len > 0) + { + if(xit != x.end()) + return 1; + if(yit != y.end()) + return -1; + } + return 0; + } + + /// Transforms input string to lower case w/o copy + static void lowerCaseInPlace(std::string &inout) { + for (unsigned int i=0; i + static Iterator partialBinarySearch(Iterator begin, Iterator end, const T& key) + { + const Iterator notFound = end; + + while(begin < end) + { + const Iterator middle = begin + (std::distance(begin, end) / 2); + + int comp = Misc::StringUtils::ciCompareLen((*middle), key, (*middle).size()); + + if(comp == 0) + return middle; + else if(comp > 0) + end = middle; + else + begin = middle + 1; + } + + return notFound; + } + + /** @brief Replaces all occurrences of a string in another string. + * + * @param str The string to operate on. + * @param what The string to replace. + * @param with The replacement string. + * @param whatLen The length of the string to replace. + * @param withLen The length of the replacement string. + * + * @return A reference to the string passed in @p str. + */ + static std::string &replaceAll(std::string &str, const char *what, const char *with, + std::size_t whatLen=std::string::npos, std::size_t withLen=std::string::npos) + { + if (whatLen == std::string::npos) + whatLen = strlen(what); + + if (withLen == std::string::npos) + withLen = strlen(with); + + std::size_t found; + std::size_t offset = 0; + while((found = str.find(what, offset, whatLen)) != std::string::npos) + { + str.replace(found, whatLen, with, withLen); + offset = found + withLen; + } + return str; + } +}; + +} + +#endif diff --git a/components/misc/utf8stream.hpp b/components/misc/utf8stream.hpp new file mode 100644 index 0000000..e499d15 --- /dev/null +++ b/components/misc/utf8stream.hpp @@ -0,0 +1,122 @@ +#ifndef MISC_UTF8ITER_HPP +#define MISC_UTF8ITER_HPP + +#include +#include + +class Utf8Stream +{ +public: + + typedef uint32_t UnicodeChar; + typedef unsigned char const * Point; + + //static const unicode_char sBadChar = 0xFFFFFFFF; gcc can't handle this + static UnicodeChar sBadChar () { return UnicodeChar (0xFFFFFFFF); } + + Utf8Stream (Point begin, Point end) : + cur (begin), nxt (begin), end (end), val(Utf8Stream::sBadChar()) + { + } + + Utf8Stream (const char * str) : + cur ((unsigned char*) str), nxt ((unsigned char*) str), end ((unsigned char*) str + strlen(str)), val(Utf8Stream::sBadChar()) + { + } + + Utf8Stream (std::pair range) : + cur (range.first), nxt (range.first), end (range.second), val(Utf8Stream::sBadChar()) + { + } + + bool eof () const + { + return cur == end; + } + + Point current () const + { + return cur; + } + + UnicodeChar peek () + { + if (cur == nxt) + next (); + return val; + } + + UnicodeChar consume () + { + if (cur == nxt) + next (); + cur = nxt; + return val; + } + + static std::pair decode (Point cur, Point end) + { + if ((*cur & 0x80) == 0) + { + UnicodeChar chr = *cur++; + + return std::make_pair (chr, cur); + } + + int octets; + UnicodeChar chr; + + std::tie (octets, chr) = octet_count (*cur++); + + if (octets > 5) + return std::make_pair (sBadChar(), cur); + + Point eoc = cur + octets; + + if (eoc > end) + return std::make_pair (sBadChar(), cur); + + while (cur != eoc) + { + if ((*cur & 0xC0) != 0x80) // check continuation mark + return std::make_pair (sBadChar(), cur); + + chr = (chr << 6) | UnicodeChar ((*cur++) & 0x3F); + } + + return std::make_pair (chr, cur); + } + +private: + + static std::pair octet_count (unsigned char octet) + { + int octets; + + unsigned char mark = 0xC0; + unsigned char mask = 0xE0; + + for (octets = 1; octets <= 5; ++octets) + { + if ((octet & mask) == mark) + break; + + mark = (mark >> 1) | 0x80; + mask = (mask >> 1) | 0x80; + } + + return std::make_pair (octets, octet & ~mask); + } + + void next () + { + std::tie (val, nxt) = decode (nxt, end); + } + + Point cur; + Point nxt; + Point end; + UnicodeChar val; +}; + +#endif diff --git a/components/process/processinvoker.cpp b/components/process/processinvoker.cpp new file mode 100644 index 0000000..cc842fd --- /dev/null +++ b/components/process/processinvoker.cpp @@ -0,0 +1,185 @@ +#include "processinvoker.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +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(); +} + +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); + mName = name; + mArguments = arguments; + + 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 + + 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 (!mProcess->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(mProcess->errorString()); + msgBox.exec(); + return false; + } + } 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(); + + return false; + } + + 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(); + + 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 new file mode 100644 index 0000000..8fff665 --- /dev/null +++ b/components/process/processinvoker.hpp @@ -0,0 +1,43 @@ +#ifndef PROCESSINVOKER_HPP +#define PROCESSINVOKER_HPP + +#include +#include +#include + +namespace Process +{ + class ProcessInvoker : public QObject + { + Q_OBJECT + + public: + + ProcessInvoker(); + ~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); + + }; +} + +#endif // PROCESSINVOKER_HPP diff --git a/components/settings/settings.cpp b/components/settings/settings.cpp new file mode 100644 index 0000000..5c70d8f --- /dev/null +++ b/components/settings/settings.cpp @@ -0,0 +1,443 @@ +#include "settings.hpp" + +#include +#include + +#include + +#include +#include + +namespace +{ + + bool parseBool(const std::string& string) + { + return (Misc::StringUtils::ciEqual(string, "true")); + } + + float parseFloat(const std::string& string) + { + std::stringstream stream; + stream << string; + float ret = 0.f; + stream >> ret; + return ret; + } + + int parseInt(const std::string& string) + { + std::stringstream stream; + stream << string; + int ret = 0; + stream >> ret; + return ret; + } + + template + std::string toString(T val) + { + std::ostringstream stream; + stream << val; + return stream.str(); + } + + template <> + std::string toString(bool val) + { + return val ? "true" : "false"; + } + +} + +namespace Settings +{ + +typedef std::map< CategorySetting, bool > CategorySettingStatusMap; + +class SettingsFileParser +{ +public: + SettingsFileParser() : mLine(0) {} + + void loadSettingsFile (const std::string& file, CategorySettingValueMap& settings) + { + mFile = file; + boost::filesystem::ifstream stream; + stream.open(boost::filesystem::path(file)); + std::cout << "Loading settings file: " << file << std::endl; + std::string currentCategory; + mLine = 0; + while (!stream.eof() && !stream.fail()) + { + ++mLine; + std::string line; + std::getline( stream, line ); + + size_t i = 0; + if (!skipWhiteSpace(i, line)) + continue; + + if (line[i] == '#') // skip comment + continue; + + if (line[i] == '[') + { + size_t end = line.find(']', i); + if (end == std::string::npos) + fail("unterminated category"); + + currentCategory = line.substr(i+1, end - (i+1)); + boost::algorithm::trim(currentCategory); + i = end+1; + } + + if (!skipWhiteSpace(i, line)) + continue; + + if (currentCategory.empty()) + fail("empty category name"); + + size_t settingEnd = line.find('=', i); + if (settingEnd == std::string::npos) + fail("unterminated setting name"); + + std::string setting = line.substr(i, (settingEnd-i)); + boost::algorithm::trim(setting); + + size_t valueBegin = settingEnd+1; + std::string value = line.substr(valueBegin); + boost::algorithm::trim(value); + + if (settings.insert(std::make_pair(std::make_pair(currentCategory, setting), value)).second == false) + fail(std::string("duplicate setting: [" + currentCategory + "] " + setting)); + } + } + + void saveSettingsFile (const std::string& file, CategorySettingValueMap& settings) + { + // No options have been written to the file yet. + CategorySettingStatusMap written; + for (CategorySettingValueMap::iterator it = settings.begin(); it != settings.end(); ++it) { + written[it->first] = false; + } + + // Have we substantively changed the settings file? + bool changed = false; + + // Were there any lines at all in the file? + bool existing = false; + + // The category/section we're currently in. + std::string currentCategory; + + // Open the existing settings.cfg file to copy comments. This might not be the same file + // as the output file if we're copying the setting from the default settings.cfg for the + // first time. A minor change in API to pass the source file might be in order here. + boost::filesystem::ifstream istream; + boost::filesystem::path ipath(file); + istream.open(ipath); + + // Create a new string stream to write the current settings to. It's likely that the + // input file and the output file are the same, so this stream serves as a temporary file + // of sorts. The setting files aren't very large so there's no performance issue. + std::stringstream ostream; + + // For every line in the input file... + while (!istream.eof() && !istream.fail()) { + std::string line; + std::getline(istream, line); + + // The current character position in the line. + size_t i = 0; + + // Don't add additional newlines at the end of the file. + if (istream.eof()) continue; + + // Copy entirely blank lines. + if (!skipWhiteSpace(i, line)) { + ostream << line << std::endl; + continue; + } + + // There were at least some comments in the input file. + existing = true; + + // Copy comments. + if (line[i] == '#') { + ostream << line << std::endl; + continue; + } + + // Category heading. + if (line[i] == '[') { + size_t end = line.find(']', i); + // This should never happen unless the player edited the file while playing. + if (end == std::string::npos) { + ostream << "# unterminated category: " << line << std::endl; + changed = true; + continue; + } + + // Ensure that all options in the current category have been written. + for (CategorySettingStatusMap::iterator mit = written.begin(); mit != written.end(); ++mit) { + if (mit->second == false && mit->first.first == currentCategory) { + std::cout << "Added new setting: [" << currentCategory << "] " + << mit->first.second << " = " << settings[mit->first] << std::endl; + ostream << mit->first.second << " = " << settings[mit->first] << std::endl; + mit->second = true; + changed = true; + } + } + + // Update the current category. + currentCategory = line.substr(i+1, end - (i+1)); + boost::algorithm::trim(currentCategory); + + // Write the (new) current category to the file. + ostream << "[" << currentCategory << "]" << std::endl; + //std::cout << "Wrote category: " << currentCategory << std::endl; + + // A setting can apparently follow the category on an input line. That's rather + // inconvenient, since it makes it more likely to have duplicative sections, + // which our algorithm doesn't like. Do the best we can. + i = end+1; + } + + // Truncate trailing whitespace, since we're changing the file anayway. + if (!skipWhiteSpace(i, line)) + continue; + + // If we've found settings before the first category, something's wrong. This + // should never happen unless the player edited the file while playing, since + // the loadSettingsFile() logic rejects it. + if (currentCategory.empty()) { + ostream << "# empty category name: " << line << std::endl; + changed = true; + continue; + } + + // Which setting was at this location in the input file? + size_t settingEnd = line.find('=', i); + // This should never happen unless the player edited the file while playing. + if (settingEnd == std::string::npos) { + ostream << "# unterminated setting name: " << line << std::endl; + changed = true; + continue; + } + std::string setting = line.substr(i, (settingEnd-i)); + boost::algorithm::trim(setting); + + // Get the existing value so we can see if we've changed it. + size_t valueBegin = settingEnd+1; + std::string value = line.substr(valueBegin); + boost::algorithm::trim(value); + + // Construct the setting map key to determine whether the setting has already been + // written to the file. + CategorySetting key = std::make_pair(currentCategory, setting); + CategorySettingStatusMap::iterator finder = written.find(key); + + // Settings not in the written map are definitely invalid. Currently, this can only + // happen if the player edited the file while playing, because loadSettingsFile() + // will accept anything and pass it along in the map, but in the future, we might + // want to handle invalid settings more gracefully here. + if (finder == written.end()) { + ostream << "# invalid setting: " << line << std::endl; + changed = true; + continue; + } + + // Write the current value of the setting to the file. The key must exist in the + // settings map because of how written was initialized and finder != end(). + ostream << setting << " = " << settings[key] << std::endl; + // Mark that setting as written. + finder->second = true; + // Did we really change it? + if (value != settings[key]) { + std::cout << "Changed setting: [" << currentCategory << "] " + << setting << " = " << settings[key] << std::endl; + changed = true; + } + // No need to write the current line, because we just emitted a replacement. + + // Curiously, it appears that comments at the ends of lines with settings are not + // allowed, and the comment becomes part of the value. Was that intended? + } + + // We're done with the input stream file. + istream.close(); + + // Ensure that all options in the current category have been written. We must complete + // the current category at the end of the file before moving on to any new categories. + for (CategorySettingStatusMap::iterator mit = written.begin(); mit != written.end(); ++mit) { + if (mit->second == false && mit->first.first == currentCategory) { + std::cout << "Added new setting: [" << mit->first.first << "] " + << mit->first.second << " = " << settings[mit->first] << std::endl; + ostream << mit->first.second << " = " << settings[mit->first] << std::endl; + mit->second = true; + changed = true; + } + } + + // If there was absolutely nothing in the file (or more likely the file didn't + // exist), start the newly created file with a helpful comment. + /*if (!existing) { + ostream << "# This is the OpenMW user 'settings.cfg' file. This file only contains" << std::endl; + ostream << "# explicitly changed settings. If you would like to revert a setting" << std::endl; + ostream << "# to its default, simply remove it from this file. For available" << std::endl; + ostream << "# settings, see the file 'settings-default.cfg' or the documentation at:" << std::endl; + ostream << "#" << std::endl; + ostream << "# https://openmw.readthedocs.io/en/master/reference/modding/settings/index.html" << std::endl; + }*/ + + // We still have one more thing to do before we're completely done writing the file. + // It's possible that there are entirely new categories, or that the input file had + // disappeared completely, so we need ensure that all settings are written to the file + // regardless of those issues. + for (CategorySettingStatusMap::iterator mit = written.begin(); mit != written.end(); ++mit) { + // If the setting hasn't been written yet. + if (mit->second == false) { + // If the catgory has changed, write a new category header. + if (mit->first.first != currentCategory) { + currentCategory = mit->first.first; + std::cout << "Created new setting section: " << mit->first.first << std::endl; + ostream << std::endl; + ostream << "[" << currentCategory << "]" << std::endl; + } + std::cout << "Added new setting: [" << mit->first.first << "] " + << mit->first.second << " = " << settings[mit->first] << std::endl; + // Then write the setting. No need to mark it as written because we're done. + ostream << mit->first.second << " = " << settings[mit->first] << std::endl; + changed = true; + } + } + + // Now install the newly written file in the requested place. + if (changed) { + std::cout << "Updating settings file: " << ipath << std::endl; + boost::filesystem::ofstream ofstream; + ofstream.open(ipath); + ofstream << ostream.rdbuf(); + ofstream.close(); + } + } + +private: + /// Increment i until it longer points to a whitespace character + /// in the string or has reached the end of the string. + /// @return false if we have reached the end of the string + bool skipWhiteSpace(size_t& i, std::string& str) + { + while (i < str.size() && std::isspace(str[i], std::locale::classic())) + { + ++i; + } + return i < str.size(); + } + + void fail(const std::string& message) + { + std::stringstream error; + error << "Error on line " << mLine << " in " << mFile << ":\n" << message; + throw std::runtime_error(error.str()); + } + + std::string mFile; + int mLine; +}; + +void Manager::clear() +{ + mDefaultSettings.clear(); + mUserSettings.clear(); + mChangedSettings.clear(); +} + +void Manager::loadDefault(const std::string &file) +{ + SettingsFileParser parser; + parser.loadSettingsFile(file, mDefaultSettings); +} + +void Manager::loadUser(const std::string &file) +{ + SettingsFileParser parser; + parser.loadSettingsFile(file, mUserSettings); +} + +void Manager::saveUser(const std::string &file) +{ + SettingsFileParser parser; + parser.saveSettingsFile(file, mUserSettings); +} + +std::string Manager::getString(const std::string &setting, const std::string &category) +{ + CategorySettingValueMap::key_type key = std::make_pair(category, setting); + CategorySettingValueMap::iterator it = mUserSettings.find(key); + if (it != mUserSettings.end()) + return it->second; + + it = mDefaultSettings.find(key); + if (it != mDefaultSettings.end()) + return it->second; + + throw std::runtime_error(std::string("Trying to retrieve a non-existing setting: ") + setting + + ".\nMake sure the settings-default.cfg file was properly installed."); +} + +float Manager::getFloat (const std::string& setting, const std::string& category) +{ + return parseFloat( getString(setting, category) ); +} + +int Manager::getInt (const std::string& setting, const std::string& category) +{ + return parseInt( getString(setting, category) ); +} + +bool Manager::getBool (const std::string& setting, const std::string& category) +{ + return parseBool( getString(setting, category) ); +} + +void Manager::setString(const std::string &setting, const std::string &category, const std::string &value) +{ + CategorySettingValueMap::key_type key = std::make_pair(category, setting); + + CategorySettingValueMap::iterator found = mUserSettings.find(key); + if (found != mUserSettings.end()) + { + if (found->second == value) + return; + } + + mUserSettings[key] = value; + + mChangedSettings.insert(key); +} + +void Manager::setInt (const std::string& setting, const std::string& category, int value) +{ + setString(setting, category, toString(value)); +} + +void Manager::setFloat (const std::string &setting, const std::string &category, float value) +{ + setString(setting, category, toString(value)); +} + +void Manager::setBool(const std::string &setting, const std::string &category, bool value) +{ + setString(setting, category, toString(value)); +} + +const CategorySettingVector Manager::apply() +{ + CategorySettingVector vec = mChangedSettings; + mChangedSettings.clear(); + return vec; +} + +} diff --git a/components/settings/settings.hpp b/components/settings/settings.hpp new file mode 100644 index 0000000..767cbea --- /dev/null +++ b/components/settings/settings.hpp @@ -0,0 +1,54 @@ +#ifndef COMPONENTS_SETTINGS_H +#define COMPONENTS_SETTINGS_H + +#include +#include +#include + +namespace Settings +{ + typedef std::pair < std::string, std::string > CategorySetting; + typedef std::set< std::pair > CategorySettingVector; + typedef std::map < CategorySetting, std::string > CategorySettingValueMap; + + /// + /// \brief Settings management (can change during runtime) + /// + class Manager + { + public: + CategorySettingValueMap mDefaultSettings; + CategorySettingValueMap mUserSettings; + + CategorySettingVector mChangedSettings; + ///< tracks all the settings that were changed since the last apply() call + + void clear(); + ///< clears all settings and default settings + + void loadDefault (const std::string& file); + ///< load file as the default settings (can be overridden by user settings) + + void loadUser (const std::string& file); + ///< load file as user settings + + void saveUser (const std::string& file); + ///< save user settings to file + + const CategorySettingVector apply(); + ///< returns the list of changed settings and then clears it + + int getInt (const std::string& setting, const std::string& category); + float getFloat (const std::string& setting, const std::string& category); + std::string getString (const std::string& setting, const std::string& category); + bool getBool (const std::string& setting, const std::string& category); + + void setInt (const std::string& setting, const std::string& category, int value); + void setFloat (const std::string& setting, const std::string& category, float value); + void setString (const std::string& setting, const std::string& category, const std::string& value); + void setBool (const std::string& setting, const std::string& category, bool value); + }; + +} + +#endif // _COMPONENTS_SETTINGS_H diff --git a/extern/json b/extern/json new file mode 160000 index 0000000..d713727 --- /dev/null +++ b/extern/json @@ -0,0 +1 @@ +Subproject commit d713727f2277f2eb919a2dbbfdd534f8988aa493 diff --git a/files/tes3mp-browser.desktop b/files/tes3mp-browser.desktop new file mode 100644 index 0000000..caf8d9e --- /dev/null +++ b/files/tes3mp-browser.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=TES3MP Server Browser +GenericName=Server Browser +Comment=Multiplayer extension for TES3. +Keywords=Morrowind;Multiplayer;Server Browser;TES;openmw; +TryExec=tes3mp-browser +Exec=tes3mp-browser +Icon=openmw +Categories=Game;RolePlaying; diff --git a/files/tes3mp/browser.qrc b/files/tes3mp/browser.qrc new file mode 100644 index 0000000..1ff60d7 --- /dev/null +++ b/files/tes3mp/browser.qrc @@ -0,0 +1,5 @@ + + + tes3mp_logo.png + + diff --git a/files/tes3mp/browser.rc b/files/tes3mp/browser.rc new file mode 100644 index 0000000..9895e8c --- /dev/null +++ b/files/tes3mp/browser.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON DISCARDABLE "tes3mp.ico" diff --git a/files/tes3mp/tes3mp-browser-default.cfg b/files/tes3mp/tes3mp-browser-default.cfg new file mode 100644 index 0000000..63b16b0 --- /dev/null +++ b/files/tes3mp/tes3mp-browser-default.cfg @@ -0,0 +1,13 @@ +[Browser] +sortByColAscending = true +maxLatency = 0 +gameMode = +noPassword = false +notFull = false +sortByCol = 0 +withPlayers = false + +[Master] +address = master.tes3mp.com +login = +password = diff --git a/files/tes3mp/tes3mp.ico b/files/tes3mp/tes3mp.ico new file mode 100644 index 0000000000000000000000000000000000000000..7938659633bd88c19b45dec5e40db465de3604a8 GIT binary patch literal 5430 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 literal 0 HcmV?d00001 diff --git a/files/tes3mp/tes3mp_logo.png b/files/tes3mp/tes3mp_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..de8812962c76f2e4b4170fa11174516c4ee690f4 GIT binary patch literal 107752 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 + + Dialog + + + + 0 + 0 + 320 + 300 + + + + Sign in + + + + + + Master server adderes + + + + + + + false + + + + + + + + + + Login + + + + + + + + + + Password + + + + + + + QLineEdit::Password + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Sign in + + + true + + + + + + + Sign up + + + + + + + Cancel + + + + + + + + diff --git a/files/tes3mp/ui/Main.ui b/files/tes3mp/ui/Main.ui new file mode 100644 index 0000000..9314481 --- /dev/null +++ b/files/tes3mp/ui/Main.ui @@ -0,0 +1,287 @@ + + + MainWindow + + + + 0 + 0 + 800 + 624 + + + + tes3mp Server Browser + + + + + + + + + + 728 + 60 + + + + + 728 + 60 + + + + + + + + + + 0 + + + + Browser + + + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + false + + + + + + + + Favorites + + + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + false + + + + + + + + + + + Filters + + + + + + + + + + Max latency: + + + + + + + + All + + + + + <50 + + + + + <100 + + + + + <150 + + + + + <200 + + + + + <250 + + + + + + + + + + + + Game mode + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Not full + + + + + + + With players + + + + + + + No password + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + true + + + toolBar + + + false + + + false + + + TopToolBarArea + + + false + + + + + + + + + + + + + + false + + + Add + + + + + false + + + Delete + + + + + Refresh + + + + + false + + + Play + + + + + Settings + + + + + Account + + + + + + diff --git a/files/tes3mp/ui/ServerInfo.ui b/files/tes3mp/ui/ServerInfo.ui new file mode 100644 index 0000000..b7c0c40 --- /dev/null +++ b/files/tes3mp/ui/ServerInfo.ui @@ -0,0 +1,297 @@ + + + Dialog + + + + 0 + 0 + 700 + 500 + + + + Connect + + + + + + + + + 192 + 192 + + + + + 192 + 192 + + + + QFrame::Box + + + QFrame::Plain + + + + + + + + + Server Name: + + + + + + + ServerName + + + + + + + Server T3MPM ID: + + + + + + + + + + true + + + + + + + Address: + + + + + + + + + + true + + + + + + + Players: + + + + + + + 0 / 0 + + + + + + + Ping: + + + + + + + -1 + + + + + + + Has DL server: + + + + + + + + + + false + + + + + + + Requires T3MPM Account: + + + + + + + + + + false + + + + + + + + + + + + + + + Players: + + + + + + + QAbstractItemView::NoSelection + + + + + + + + + + + Content Files: + + + + + + + QAbstractItemView::NoSelection + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + false + + + + + + + Refresh + + + + + + + false + + + Connect + + + true + + + + + + + + + + + Additional Info: + + + + + + + + + + + + + + + + btnCancel + clicked() + Dialog + reject() + + + 489 + 524 + + + 316 + 274 + + + + + btnConnect + clicked() + Dialog + accept() + + + 580 + 524 + + + 316 + 274 + + + + + diff --git a/files/tes3mp/ui/Settings.ui b/files/tes3mp/ui/Settings.ui new file mode 100644 index 0000000..06f9892 --- /dev/null +++ b/files/tes3mp/ui/Settings.ui @@ -0,0 +1,625 @@ + + + DialogSettings + + + + 0 + 0 + 800 + 490 + + + + Settings + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + QTabWidget::North + + + QTabWidget::Rounded + + + 0 + + + Qt::ElideNone + + + + Client + + + + + + + + Default Server + + + + + + + + + + Address + + + + + + + 127.0.0.1 + + + + + + + + + + + Port + + + + + + + 25565 + + + 5 + + + + + + + + + + + + + Password + + + + + + + + + + + + + + + + + Log Level + + + + + + + + Off + + + + + Fatal errors + + + + + General errors + + + + + Warnings + + + + + Info + + + + + Verbose + + + + + Trace + + + + + + + + + + + + Chat + + + + + + + + + + Key switch mode + + + + + + + F2 + + + + + + + + + + + Key + + + + + + + Y + + + + + + + + + + + Delay + + + + + + + 1 + + + 0.500000000000000 + + + 101.000000000000000 + + + 5.000000000000000 + + + + + + + + + + + Window position + + + + + + + + W + + + + + + + 65535 + + + + + + + + + + + X + + + + + + + 65535 + + + + + + + + + + + Y + + + + + + + 65535 + + + + + + + + + + + H + + + + + + + 65535 + + + + + + + + + + + + + + + + Server + + + + + + General + + + + + + Address + + + + + + + + Broadcast address + + + + + + + 0.0.0.0 + + + + + + + + + + + Port + + + + + + + 25565 + + + 5 + + + + + + + + + + + + + + Hostname + + + + + + + My TES3MP server + + + + + + + + + + + Maximum players + + + + + + + 1 + + + 655535 + + + + + + + + + + + Password + + + + + + + + + + + + + + Log Level + + + + + + + + Off + + + + + Fatal errors + + + + + General errors + + + + + Warnings + + + + + Info + + + + + Verbose + + + + + Trace + + + + + + + + + + + + + true + + + Modules + + + + + + AutoSort + + + true + + + + + + + true + + + + + + + + + + + + + + Add + + + + + + + + + + + Up + + + + + + + Down + + + + + + + Remove + + + + + + + + + + + + + + Path + + + + + + + + + + ... + + + + + + + + + + + + + + + + + + buttonBox + accepted() + DialogSettings + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + DialogSettings + reject() + + + 316 + 260 + + + 286 + 274 + + + + +