diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..16b9b54 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,134 @@ +kind: pipeline +name: arch-g++ + +steps: +- name: build-and-test + image: muttleyxd/a3ul_archlinux_build:latest + commands: + - mkdir /tmp/build && cd /tmp/build + - cmake /drone/src -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-Werror" -DRUN_TESTS=ON -DDEVELOPER_MODE=ON + - make -j$(nproc) + - ctest -V + +--- +kind: pipeline +name: ubuntu-18.04-g++-8 + +steps: +- name: build-and-test + image: muttleyxd/a3ul_ubuntu-18.04_build:latest + commands: + - mkdir /tmp/build && cd /tmp/build + - cmake /drone/src -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-Werror" -DCMAKE_CXX_COMPILER=g++-8 -DRUN_TESTS=ON -DDEVELOPER_MODE=ON + - make -j$(nproc) + - ctest -V + +--- +kind: pipeline +name: arch-g++-package + +clone: + disable: true + +steps: +- name: clone + image: alpine/git + commands: + - git clone $DRONE_GIT_HTTP_URL . + - git checkout $DRONE_COMMIT + - chmod 777 /tmp/build + volumes: + - name: package + path: /tmp/build +- name: build-test-makepkg + image: muttleyxd/a3ul_archlinux_build:latest + volumes: + - name: package + path: /tmp/build + commands: + - cp -r /drone/src /tmp/arma3-unix-launcher + - mkdir -p /tmp/build && cd /tmp/build + - cp /tmp/arma3-unix-launcher/tools/ci/packaging/archlinux/PKGBUILD ./ + - sed -i 's|/arma3-unix-launcher|/tmp/arma3-unix-launcher|g' PKGBUILD + - makepkg +- name: publish_release + image: muttleyxd/github-release + environment: + GITHUB_PUBLISH_TOKEN: + from_secret: github_publish_key + commands: + - cd /tmp/build + - mv $(echo *.tar.xz) $(echo *.tar.xz | sed 's/x86_64.pkg.tar.xz/archlinux-x86_64.pkg.tar.xz/g') + - github-release --token $GITHUB_PUBLISH_TOKEN --repository muttleyxd/arma3-unix-launcher --file-glob "/tmp/build/*.tar.xz" + when: + event: [push, tag, deployment] + branch: + - master + volumes: + - name: package + path: /tmp/build + +volumes: +- name: package + temp: {} + +--- +kind: pipeline +name: ubuntu-g++-8-package + +clone: + disable: true + +steps: +- name: clone + image: alpine/git + commands: + - git clone $DRONE_GIT_HTTP_URL . + - git checkout $DRONE_COMMIT + - chmod 777 /tmp/build + volumes: + - name: package + path: /tmp/build +- name: build-test-makepkg + image: muttleyxd/a3ul_ubuntu-18.04_build:latest + volumes: + - name: package + path: /tmp/build + commands: + - . /etc/lsb-release + - . /etc/os-release + - export SHORT_HASH=`git rev-parse --verify HEAD | cut -c -7` + - export COMMIT_COUNT=`git rev-list HEAD --count` + - export TMP_BUILD="/tmp/build_deb" + - export PKG_DIR="$TMP_BUILD/arma3-unix-launcher-$COMMIT_COUNT.$SHORT_HASH-$ID-$VERSION_ID-amd64" + - mkdir -p $PKG_DIR/DEBIAN + - cd $PKG_DIR + - sed 's/VERSION/$COMMIT_COUNT-$SHORT_HASH/g' /drone/src/tools/ci/packaging/ubuntu-18.04/control >./DEBIAN/control + - mkdir $TMP_BUILD/cmake_build && cd $TMP_BUILD/cmake_build + - cmake /drone/src -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_INSTALL_PREFIX=/usr -DRUN_TESTS=ON + - make -j$(nproc) + - ctest --output-on-failure + - make install DESTDIR=$PKG_DIR + - cd $TMP_BUILD + - dpkg-deb --build $PKG_DIR + - ls -alh + - cp *.deb /tmp/build +- name: publish_release + image: muttleyxd/github-release + when: + event: [push, tag, deployment] + branch: + - master + environment: + GITHUB_PUBLISH_TOKEN: + from_secret: github_publish_key + commands: + - cd /tmp/build + - github-release --token $GITHUB_PUBLISH_TOKEN --repository muttleyxd/arma3-unix-launcher --file-glob "/tmp/build/*.deb" + volumes: + - name: package + path: /tmp/build + +volumes: +- name: package + temp: {} diff --git a/.gitignore b/.gitignore index 41115bf..12fb044 100644 --- a/.gitignore +++ b/.gitignore @@ -1,48 +1,12 @@ -# Compiled Object files -*.slo -*.lo -*.o -*.obj +/build* -# Precompiled Headers -*.gch -*.pch +#Qt Creator +CMakeLists.txt.* -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app - -#Executable -arma3-unix-launcher - -#Eclipse project files -.settings/* -.cproject -.project -/MainForm.glade~ -/#MainForm.glade# - -#CLion project files +#CLion .idea/* +cmake-build*/ -#build dir -build/* -cmake-build-debug/* -cmake-build-release/* -CMakeLists.txt.user +#Packages +*.tar.xz +*.deb diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f94a370 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +git: + depth: false #($COMMIT_COUNT would fail otherwise) +os: +- osx +language: cpp +script: +- mkdir build && cd build +- cmake .. -DCPACK_GENERATOR=DragNDrop -DCMAKE_PREFIX_PATH='/usr/local;/usr/local/opt/qt' + -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-Werror" -DCMAKE_CXX_COMPILER=g++-9 + -DRUN_TESTS=ON -DDEVELOPER_MODE=ON +- set -e +- make -j$(sysctl -n hw.ncpu) +- ctest +- make package +- export COMMIT_COUNT=`git rev-list --count HEAD` +- export COMMIT_HASH=`git rev-parse --verify HEAD | cut -c -7` +- mv arma3-unix-launcher-*.dmg arma3-unix-launcher-$COMMIT_COUNT-$COMMIT_HASH-mac_os_x-x86_64.dmg +deploy: + provider: releases + api_key: + secure: il+wwZH4DkB2kXPp4vgGmtWUKh4y6O+Jt415mcbHHTZLx0a6HaoJnVRLXMXEkJ9lqw9wlME2phlAcDNprSZrkTAH7lnz9ZgCklcIMIgPkvKJMleH1rUBy5YwBrYsld1XhjEbbxWzBGSJr0AXlAUKhB5ApwME0c8GW1awe4Job48cmhY/wUqm7PXv8opx4L871bhyXRzW1xxnB0HMw8Ru/VZ/c/tuo7zvy5m2ilia9+C+XW1xTI3nn6TNyEk1w3DD5dMWbu6ZBIKJZFuoZFe/Ysjps15ihJVpRjJ8ZAPJYu5FChRnBSYbdmeB5gOw1B5x+PJXCxnNbjUAfQ3NOCO+2Y+v/37QC9EYZ5FELKEuMTA2nbF1D6gC1bk9uA+V5PbEhFsMkSyrpY8xsWCKCwFJlI6AEJHKSJ+zDPanuNGSsJJbwKFI846z8bEFPY8ORj6wFzcMiiY0a1pFoOKmOLIUedvEX176MEOwyyjZbWqQF/5HA70XBuPbS0f9idVNBX5PkRUIEU02z2+K/hGZI35zHXJY1bmk5FNkrQYHmhSS/5AibIKEFBZlJ5bIHntP0V5wgE9VIO87xFFQyq4j6Xc95r3EfMilkOqOnWQrVLDYT1BB59a4rkWimc83FBNvT/RlrvBmRskg8g/ZvIlSHnBpwpBngEDBEqiztQlRWKva+n0= + draft: true + file_glob: true + file: "/Users/travis/build/muttleyxd/arma3-unix-launcher/build/*.dmg" + skip_cleanup: true + on: + repo: muttleyxd/arma3-unix-launcher + branch: master diff --git a/CMakeLists.txt b/CMakeLists.txt index 07c3d09..7cdacd4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,33 +1,35 @@ -cmake_minimum_required (VERSION 2.8.12) -project (arma3-unix-launcher) - -set (CMAKE_CXX_STANDARD 11) - -# GTKMM -find_package (PkgConfig REQUIRED) -pkg_check_modules (GTKMM REQUIRED gtkmm-3.0) -include_directories(SYSTEM ${GTKMM_INCLUDE_DIRS}) -link_directories (${GTKMM_LIBRARY_DIRS}) - -#build executable -add_executable (arma3-unix-launcher main.cpp Filesystem.cpp Logger.cpp MainWindow.cpp Mod.cpp Settings.cpp Utils.cpp VDF.cpp VDFKey.cpp) -target_link_libraries (arma3-unix-launcher ${GTKMM_LIBRARIES} pthread) -set (CMAKE_CXX_FLAGS "-std=gnu++11 ${CMAKE_CXX_FLAGS}") - -if(APPLE) - add_custom_command (TARGET arma3-unix-launcher POST_BUILD COMMAND mkdir arma3-unix-launcher.app && cd arma3-unix-launcher.app && mkdir Contents && cd Contents && mkdir MacOS) - add_custom_command (TARGET arma3-unix-launcher POST_BUILD COMMAND cp arma3-unix-launcher ${CMAKE_CURRENT_BINARY_DIR}/arma3-unix-launcher.app/Contents/MacOS) - add_custom_command (TARGET arma3-unix-launcher POST_BUILD COMMAND cp ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist ${CMAKE_CURRENT_BINARY_DIR}/arma3-unix-launcher.app/Contents) - add_custom_command (TARGET arma3-unix-launcher POST_BUILD COMMAND cp ${CMAKE_CURRENT_SOURCE_DIR}/MainForm.glade ${CMAKE_CURRENT_BINARY_DIR}/arma3-unix-launcher.app/Contents/MacOS) -else(APPLE) - add_custom_command (TARGET arma3-unix-launcher POST_BUILD COMMAND cp ${CMAKE_CURRENT_SOURCE_DIR}/MainForm.glade ${CMAKE_CURRENT_BINARY_DIR}/MainForm.glade) -endif(APPLE) - -# Installation -if(APPLE) - install (DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/arma3-unix-launcher.app DESTINATION /Applications USE_SOURCE_PERMISSIONS) -else(APPLE) - install (TARGETS arma3-unix-launcher DESTINATION /usr/bin) - install (FILES ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}.desktop DESTINATION /usr/share/applications) - install (FILES ${CMAKE_CURRENT_SOURCE_DIR}/MainForm.glade DESTINATION /usr/share/arma3-unix-launcher) -endif(APPLE) +cmake_minimum_required(VERSION 3.11) +project(arma3-unix-launcher) + +include(cmake/external_dependencies.cmake) +include(cmake/functions.cmake) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic") +add_libraries_to_linker(COMPILER_ID GNU + LIBS stdc++fs + WHEN ${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 9 + WARNING_MESSAGE "Applying workaround for GCC 8 and lower - linking stdc++fs") + +option(DEVELOPER_MODE "Enable developer checks" OFF) +option(RUN_TESTS "Run tests with DocTest" OFF) + +if (DEVELOPER_MODE) + add_definitions(-DSTATIC_TODO_ENABLED) +endif () + +include_directories(external/static-todo) +setup_argparse() +setup_fmt() +setup_nlohmann_json() + +add_subdirectory(src) + +if (RUN_TESTS) + setup_doctest() + setup_trompeloeil() + enable_testing() + add_subdirectory(tests) +endif () + +include(external/use-backward-cpp.cmake) diff --git a/Filesystem.cpp b/Filesystem.cpp deleted file mode 100644 index 311e791..0000000 --- a/Filesystem.cpp +++ /dev/null @@ -1,478 +0,0 @@ -/* - * Filesystem.cpp - * - * Created on: 14 Oct 2016 - * Author: muttley - */ - -#include "Filesystem.h" - -#include -#include -#include -#include - -#include -#include -#include -#include - -#include - -#include "VDF.h" -#include "Utils.h" -#include "Logger.h" - -using namespace std; - -namespace Filesystem -{ - std::string FILE_NOT_OPEN = "FILE_NOT_OPEN"; - std::string DIR_NOT_FOUND = "DIR_NOT_FOUND"; - std::string NOT_A_SYMLINK = "NOT_A_SYMLINK"; - -#ifdef __APPLE__ - std::string LocalSharePrefix = "/Library/Application Support"; - std::string BohemiaInteractivePrefix = "/com.vpltd.Arma3"; - std::string SteamPath = LocalSharePrefix + "/Steam"; - std::string LauncherSettingsDirectory = LocalSharePrefix + "/a3unixlauncher"; -#else - std::string LocalSharePrefix = "/.local/share"; - std::string BohemiaInteractivePrefix = "/bohemiainteractive/arma3"; - std::string SteamPath = "/.steam/steam"; - std::string LauncherSettingsDirectory = "/.config/a3unixlauncher"; -#endif - std::string HomeDirectory = getenv("HOME"); - - std::string SteamConfigFile = HomeDirectory + LocalSharePrefix + "/Steam/config/config.vdf"; - std::string SteamConfigFileNeon = HomeDirectory + "/.steam/steam/config/config.vdf"; - std::string SteamAppsArmaPath = "/steamapps/common/Arma 3"; - std::string SteamAppsModWorkshopPath = "/steamapps/workshop/content/107410"; - - std::string LauncherSettingsFilename = "/settings.conf"; - - std::string ArmaDirWorkshop = "/!workshop"; - std::string ArmaDirCustom = "/!custom"; - std::string ArmaDirDoNotChange = "/!DO_NOT_CHANGE_FILES_IN_THESE_FOLDERS"; - std::string ArmaDirMark = "~arma"; - - std::string ArmaConfigFile = HomeDirectory + LocalSharePrefix + BohemiaInteractivePrefix + - "/GameDocuments/Arma 3/Arma3.cfg"; - - std::vector GetSteamLibraries() - { - vector response; - string steamConfigFile; - ifstream configFile; - - std::string config_file_path = SteamConfigFile; - if (!FileExists(config_file_path)) - config_file_path = SteamConfigFileNeon; - - configFile.open(config_file_path, ios::in); - if (configFile.is_open()) - { - LOG(0, "File " + SteamConfigFile + " successfully opened...\nReading libraries list...\n"); - getline(configFile, steamConfigFile, '\0'); - VDF vdfReader(steamConfigFile); - string currentLibraryPath = ""; - int libraryNumber = 1; - - while (true) - { - currentLibraryPath = vdfReader.GetValue("InstallConfigStore/Software/Valve/Steam/BaseInstallFolder_" + to_string( - libraryNumber++)); - if (currentLibraryPath == KEY_NOT_FOUND) - break; - response.push_back(currentLibraryPath); - } - } - else - LOG(1, "Can't open " + SteamConfigFile + "\nCritical error\n"); - - string home_steam_dir = HomeDirectory + LocalSharePrefix + "/Steam/steamapps/common"; - if (DirectoryExists(home_steam_dir)) - response.push_back(home_steam_dir); - return response; - } - - std::string GetDirectory(DirectoryToFind dtf) - { - string DirName = ""; - switch (dtf) - { - case DirectoryToFind::ArmaInstall: - DirName = SteamAppsArmaPath; - break; - case DirectoryToFind::WorkshopMods: - DirName = SteamAppsModWorkshopPath; - break; - default: - DirName = SteamAppsModWorkshopPath; - break; - } - DIR *dir = opendir((HomeDirectory + SteamPath + DirName).c_str()); - if (dir) - { - closedir(dir); - return HomeDirectory + SteamPath + DirName; - } - for (string s : GetSteamLibraries()) - { - DIR *dir = opendir((s + DirName).c_str()); - if (dir) - { - closedir(dir); - return s + DirName; - } - } - return DIR_NOT_FOUND; - } - - bool FileExists(string path) - { - struct stat buffer; - return (stat(path.c_str(), &buffer) == 0); - } - - bool DirectoryExists(string path) - { - struct stat buffer; - int errorCode = stat(path.c_str(), &buffer); - if (errorCode == -1) - { - if (errno == ENOENT) - return false; - LOG(1, "Stat Error in DirectoryExists(" + path + ");"); - return false; - } - else - { - if (S_ISDIR(buffer.st_mode)) - return true; - return false; - } - } - - bool WriteAllText(string path, string value) - { - LOG(0, "Writing to file " + path); - ofstream outFile(path); - if (outFile.is_open()) - { - LOG(0, "File write success"); - outFile << value; - outFile.close(); - return true; - } - LOG(1, "Can't open file " + path + " for write"); - return false; - } - - string ReadAllText(string path, bool suppress_log) - { - if (!suppress_log) - LOG(0, "Reading file " + path + "... "); - string response; - ifstream inFile; - inFile.open(path, ios::in); - if (inFile.is_open()) - { - getline(inFile, response, '\0'); - inFile.close(); - if (!suppress_log) - LOG(0, "File read successfully"); - return response; - } - if (!suppress_log) - LOG(1, "Can't open file " + path + " for read"); - return FILE_NOT_OPEN; - } - - vector FindMods(string path) - { - vector response; - - for (string s : GetSubDirectories(path)) - { - if (s == "Curator" || s == "Dta" || s == "Expansion" || s == "Heli" - || s == "Jets" || s == "Kart" || s == "Mark" || s == "Argo" - || s == "Orange" || s == "Tacops" || s == "Tank") //skip DLCs - continue; - if (Utils::ContainsAddons(path + "/" + s) && Utils::ContainsCppFile(path + "/" + s)) - { - if (isdigit(s.at(0))) - response.push_back(Mod(path + "/" + s, s)); - else - response.push_back(Mod(path + "/" + s, "-1")); - } - } - return response; - } - - void CheckFileStructure(string armaDir, string workshopDir, vector modList) - { - LOG(0, "Checking file structure"); - string armaDirWorkshopPath = armaDir + Filesystem::ArmaDirWorkshop; - string armaDirCustomPath = armaDir + Filesystem::ArmaDirCustom; - - LOG(0, "!workshop -> " + armaDirWorkshopPath + "\n!custom -> " + armaDirCustomPath); - - if (!DirectoryExists(armaDirWorkshopPath)) - { - if (!CreateDirectory(armaDirWorkshopPath)) - return; - } - - if (!DirectoryExists(armaDirWorkshopPath + Filesystem::ArmaDirDoNotChange)) - { - if (!CreateDirectory(armaDirWorkshopPath + Filesystem::ArmaDirDoNotChange)) - return; - } - - if (!DirectoryExists(armaDirCustomPath)) - { - if (!CreateDirectory(armaDirCustomPath)) - return; - } - - if (!DirectoryExists(armaDirCustomPath + Filesystem::ArmaDirDoNotChange)) - { - if (!CreateDirectory(armaDirCustomPath + Filesystem::ArmaDirDoNotChange)) - return; - } - - vector ModDirs = GetSubDirectories(armaDirWorkshopPath); - CheckSymlinks(armaDirWorkshopPath, armaDir, workshopDir, &ModDirs, &modList); - - ModDirs = GetSubDirectories(armaDirCustomPath); - CheckSymlinks(armaDirCustomPath, armaDir, workshopDir, &ModDirs, &modList); - - for (Mod m : modList) - { - if (m.Path.find(armaDir) != string::npos) - continue; - if (!m.IsRepresentedBySymlink) - { - string linkName = armaDirWorkshopPath + "/@" + m.DirName; - if (m.WorkshopId == "-1") - linkName = armaDirCustomPath + "/@" + m.DirName; - if (!DirectoryExists(linkName) || !FileExists(linkName)) - { - int result = symlink(m.Path.c_str(), linkName.c_str()); - if (result != 0) - LOG(1, "Symlink creation failed: " + m.Path + "->" + linkName); - else - LOG(0, "Symlink creation success: " + m.Path + "->" + linkName); - } - else - LOG(1, "Dir/file " + linkName + " already exists"); - } - } - } - - vector GetSubDirectories(string path) - { - vector response; - struct dirent *directoryEntry; - - DIR *pathDir = opendir(path.c_str()); - - if (pathDir == NULL) - { - LOG(1, "Opening directory " + path + " failed"); - return response; - } - - while ((directoryEntry = readdir(pathDir)) != NULL) - { - struct stat st; - - if ((strcmp(directoryEntry->d_name, ".") == 0) || (strcmp(directoryEntry->d_name, "..") == 0)) - continue; - - if (fstatat(dirfd(pathDir), directoryEntry->d_name, &st, 0) < 0) - { - int error = errno; - string DirName = directoryEntry->d_name; - DirName += " strerror: "; - DirName += strerror(errno); - LOG(1, "Directory error " + DirName); - continue; - } - - if (S_ISDIR(st.st_mode)) - response.push_back(directoryEntry->d_name); - } - - closedir(pathDir); - - return response; - } - - bool CreateDirectory(string path) - { - int status = mkdir(path.c_str(), S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); //d rwx r-x r-x - if (status != 0) - { - LOG(1, "Can't create directory " + path); - return false; - } - LOG(0, "Successfully created directory " + path); - return true; - } - - string GetSymlinkTarget(string path) - { - struct stat statinfo; - if (lstat(path.c_str(), &statinfo) < 0) - { - LOG(0, "Can't open directory/symlink " + path); - return NOT_A_SYMLINK; - } - if (S_ISLNK(statinfo.st_mode)) - { - char *buffer = new char[PATH_MAX + 1]; - size_t pathLength = readlink(path.c_str(), buffer, PATH_MAX); - buffer[pathLength] = '\0'; - string target = buffer; - delete[] buffer; - LOG(0, "!workshop/" + path + " points to: " + target); - - return target; - } - return NOT_A_SYMLINK; - } - - void CheckSymlinks(std::string path, std::string armaDir, std::string workshopDir, vector *ModDirs, - vector *modList) - { - for (int i = 0; i < ModDirs->size(); i++) - { - string s = ModDirs->operator [](i); - string target = GetSymlinkTarget(path + "/" + s); - if (target != NOT_A_SYMLINK) - { - int targetLength = target.length(); - target = Utils::Replace(target, workshopDir + "/", ""); - - //outside workshop dir - if (targetLength == target.length()) - { - for (int i = 0; i < modList->size(); i++) - { - if (modList->operator[](i).Path == target) - { - modList->operator[](i).IsRepresentedBySymlink = true; - break; - } - } - } - else - { - string newWorkshopId = target; - for (int i = 0; i < modList->size(); i++) - { - if (modList->operator[](i).WorkshopId == newWorkshopId && s == modList->operator[](i).Name) - { - modList->operator[](i).IsRepresentedBySymlink = true; - break; - } - } - } - } - else - LOG(1, "Not a symlink found in ModDirs!"); - } - } - - string GenerateArmaCfg(string armaPath, string source, vector modList) - { - LOG(1, "Generating Arma3.cfg"); - string response; - string inputFile = ReadAllText(source); - if (inputFile == FILE_NOT_OPEN) - { - inputFile = ""; - std::string command = "mkdir -p $(realpath -m $(dirname \"" + source + "\"))"; - LOG(1, "Cannot read Arma3.cfg, creating directory " + command); - system(command.c_str()); - } - - string modLauncherList = "class ModLauncherList\n{\n"; - int i; - - for (i = 0; i < modList.size(); i++) - { - string fullPath = armaPath; - string symlinkAt = "@"; - if (modList[i]->WorkshopId == "-1") - { - if (fullPath.find(armaPath) == std::string::npos) - fullPath += Filesystem::ArmaDirCustom; - else - symlinkAt = ""; - } - else - fullPath += Filesystem::ArmaDirWorkshop; - fullPath += "/" + symlinkAt + modList[i]->DirName; - string testPath = GetSymlinkTarget(fullPath); - if (testPath != NOT_A_SYMLINK) - { - fullPath = testPath; - }; - string dirName = symlinkAt + modList[i]->DirName; - - string disk = "C:"; - if (IsProton(armaPath)) - disk = "Z:"; - string windowsPath = disk + Utils::Replace(fullPath, "/", "\\"); - modLauncherList += "\tclass Mod" + to_string(i + 1) + "\n\t{" - + "\n\t\tdir=\"" + dirName + "\";" - + "\n\t\tname=\"" + modList[i]->Name + "\";" - + "\n\t\torigin=\"GAME DIR\";" - + "\n\t\tfullPath=\"" + windowsPath + "\";" - + "\n\t};\n"; - } - - modLauncherList += "};"; - - int leftBracketsOpen = 0; - bool ignoreAll = false; - - vector lines = Utils::Split(inputFile, "\n"); - for (string s : lines) - { - string logMsg = "Line: " + s + "\n"; - if (Utils::Trim(s) == "{") - { - leftBracketsOpen++; - logMsg += "Left brackets opened: " + to_string(leftBracketsOpen); - } - else if (Utils::Trim(s) == "};") - { - leftBracketsOpen--; - if (leftBracketsOpen == 0) - ignoreAll = false; - logMsg += "Left brackets opened: " + to_string(leftBracketsOpen) - + "\nignoreAll = " + Utils::ToString(ignoreAll); - } - else - { - if (Utils::Trim(s) == "class ModLauncherList") - ignoreAll = true; - if (!ignoreAll) - response += s + "\n"; - logMsg += "\nignoreAll = " + Utils::ToString(ignoreAll); - } - LOG(0, logMsg); - } - response += modLauncherList + "\n"; - return response; - } - - bool IsProton(std::string path) - { - return Filesystem::FileExists(path + "/arma3launcher.exe"); - } -} diff --git a/Filesystem.h b/Filesystem.h deleted file mode 100644 index e6d433f..0000000 --- a/Filesystem.h +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Filesystem.h - * - * Created on: 14 Oct 2016 - * Author: muttley - */ - -#ifndef FILESYSTEM_H_ -#define FILESYSTEM_H_ - -#include -#include -#include -#include "Mod.h" - -enum DirectoryToFind -{ - ArmaInstall = 0, - WorkshopMods = 1 -}; - -namespace Filesystem -{ - extern std::string FILE_NOT_OPEN; - extern std::string DIR_NOT_FOUND; - extern std::string NOT_A_SYMLINK; - - extern std::string LocalSharePrefix; - extern std::string BohemiaInteractivePrefix; - extern std::string SteamPath; - extern std::string LauncherSettingsDirectory; - - extern std::string SteamConfigFile; - extern std::string SteamConfigFileNeon; - extern std::string SteamAppsArmaPath; - extern std::string SteamAppsModWorkshopPath; - extern std::string SteamPath; - extern std::string HomeDirectory; - extern std::string LauncherSettingsDirectory; - extern std::string LauncherSettingsFilename; - - extern std::string ArmaDirWorkshop; - extern std::string ArmaDirCustom; - - extern std::string ArmaDirDoNotChange; - - extern std::string ArmaDirMark; - - extern std::string ArmaConfigFile; - - //profiles - *.profile files in settings directory - - std::vector GetSteamLibraries(); - std::string GetDirectory(DirectoryToFind dtf); - - bool FileExists(std::string path); - bool DirectoryExists(std::string path); - - bool WriteAllText(std::string path, std::string value); - std::string ReadAllText(std::string path, bool suppress_log = false); - - std::vector FindMods(std::string path); - - void CheckFileStructure(std::string armaDir, std::string workshopDir, std::vector modList); - - std::vector GetSubDirectories(std::string path); - - bool CreateDirectory(std::string path); - - std::string GetSymlinkTarget(std::string path); - void CheckSymlinks(std::string path, std::string armaDir, std::string workshopDir, std::vector *ModDirs, - std::vector *modList); - - std::string GenerateArmaCfg(std::string armaPath, std::string source, std::vector modList); - - bool IsProton(std::string path); -} -#endif /* FILESYSTEM_H_ */ diff --git a/Info.plist b/Info.plist deleted file mode 100644 index a993230..0000000 --- a/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - BuildMachineOSBuild - 15G31 - CFBundleDevelopmentRegion - English - CFBundleExecutable - arma3-unix-launcher - CFBundleIdentifier - muttley.a3unixlauncher - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Arma 3 Unix Launcher - CFBundlePackageType - APPL - CFBundleShortVersionString - 21 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSMinimumSystemVersion - 10.10 - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/LICENSE b/LICENSE index f234d61..f13eaa9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 Muttley +Copyright (c) 2016-2020 Mateusz Szychowski (Muttley) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Logger.cpp b/Logger.cpp deleted file mode 100644 index 7dd0217..0000000 --- a/Logger.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Logger.cpp - * - * Created on: 29 Oct 2016 - * Author: muttley - */ -#include "Logger.h" -#include -#include - -using namespace std; - -int LogLevel = 1; - -void LOG(int logLevel, string text) -{ - time_t rawTime; - tm *timeInfo; - char buffer[30]; - - time(&rawTime); - timeInfo = localtime(&rawTime); - - strftime(buffer, 30, "[%F %T] ", timeInfo); - if (logLevel >= LogLevel) - cout << buffer << text << endl; -} - - - diff --git a/Logger.h b/Logger.h deleted file mode 100644 index 674cea5..0000000 --- a/Logger.h +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Logger.h - * - * Created on: 29 Oct 2016 - * Author: muttley - */ - -#ifndef LOGGER_H_ -#define LOGGER_H_ - -#include - -//0 - full verbose output -//1 - limited output - -//1 is default - -extern int LogLevel; -void LOG(int logLevel, std::string text); - -#endif /* LOGGER_H_ */ diff --git a/MainForm.glade b/MainForm.glade deleted file mode 100644 index cfbd14f..0000000 --- a/MainForm.glade +++ /dev/null @@ -1,1382 +0,0 @@ - - - - - - 1 - 32 - 1 - 1 - 10 - - - 100 - 1 - 10 - - - - - - - - - - - - - - - - - - - - - - - False - - - - - - True - False - vertical - - - True - True - True - True - - - True - False - vertical - - - True - False - Workshop mods - Here you can see mods you are subscribed to - - - - - - False - True - 0 - - - - - True - True - True - in - - - True - True - True - True - adjustment1 - natural - workshopModsStore - True - horizontal - - - - - - True - Enabled - True - True - 0 - - - - 0 - - - - - - - True - Name - True - True - 1 - - - - 1 - - - - - - - True - Workshop ID - True - True - 2 - - - - 2 - - - - - - - - - False - True - 1 - - - - - True - False - Custom mods - Here you can add your own mods - - - - - - False - True - 2 - - - - - True - True - True - in - - - True - True - True - True - customModsStore - True - horizontal - - - - - - Enabled - True - True - 0 - - - - 0 - - - - - - - Name - True - True - 1 - - - - 1 - - - - - - - Path - True - True - 2 - - - - 2 - - - - - - - - - False - True - 3 - - - - - True - False - True - - - True - False - Custom Mods: - - - False - True - 0 - - - - - gtk-add - True - True - True - True - True - - - False - True - 1 - - - - - gtk-remove - True - True - True - True - True - - - False - True - 2 - - - - - False - True - 4 - - - - - - - True - False - True - True - Mods - - - False - - - - - True - True - in - - - True - False - - - True - False - vertical - - - True - True - - - True - False - vertical - - - Show static background in menu - True - True - False - start - True - - - False - True - 0 - - - - - True - False - - - False - True - 4 - 1 - - - - - Skip logos at startup - True - True - False - start - True - - - False - True - 2 - - - - - True - False - - - False - True - 4 - 3 - - - - - Force window mode - True - True - False - start - True - - - False - True - 4 - - - - - True - False - - - False - True - 4 - 5 - - - - - True - False - - - Profile - True - True - False - start - True - - - False - True - 0 - - - - - True - False - True - True - - - False - True - 4 - 1 - - - - - False - True - 6 - - - - - - - True - False - Basic - - - - - - - - False - True - 0 - - - - - True - True - - - True - False - vertical - - - True - False - - - Parameter file - True - True - False - start - 20 - True - - - False - True - 0 - - - - - True - False - True - True - - - False - True - 4 - 1 - - - - - gtk-open - True - False - True - True - True - True - - - False - True - 4 - 2 - - - - - False - True - 0 - - - - - True - False - - - False - True - 4 - 1 - - - - - Check signatures - cbCheckSignatures - True - True - False - start - True - - - False - True - 2 - - - - - True - False - - - False - True - 4 - 3 - - - - - True - False - - - CPU count - cbCpuCount - True - True - False - start - 42 - True - - - False - True - 0 - - - - - numCpuCount - True - False - True - True - 1 - adjCpuCount - 1 - True - 1 - - - False - True - 4 - 1 - - - - - False - True - 4 - - - - - True - False - - - False - True - 4 - 5 - - - - - True - False - True - - - Extra threads - cbExThreads - True - True - False - start - True - - - False - True - 0 - - - - - True - False - vertical - - - File operations - cbExThreadsFileOperations - True - False - True - False - start - True - - - False - True - 0 - - - - - Texture loading - cbExThreadsTextureLoading - True - False - True - False - start - True - - - False - True - 1 - - - - - Geometry loading - cbExThreadsGeometryLoading - True - False - True - False - start - True - - - False - True - 2 - - - - - False - True - 1 - - - - - False - True - 6 - - - - - True - False - - - False - True - 4 - 7 - - - - - Enable Hyper-Threading - cbEnableHT - True - True - False - start - True - - - False - True - 8 - - - - - True - False - - - False - True - 4 - 9 - - - - - Disable Multicore Rendering - cbDisableMulticore - True - True - False - start - True - - - False - True - 10 - - - - - True - False - - - False - True - 4 - 11 - - - - - Enable Huge Pages - cbHugePages - True - True - False - start - True - - - False - True - 12 - - - - - True - False - - - False - True - 4 - 13 - - - - - Enable File-Patching - cbFilePatching - True - True - False - start - True - - - False - True - 14 - - - - - True - False - - - False - True - 4 - 15 - - - - - No Logs - cbNoLogs - True - True - False - start - True - - - False - True - 16 - - - - - True - False - - - False - True - 4 - 17 - - - - - Show Script Errors - cbShowScriptErrors - True - True - False - start - True - - - False - True - 18 - - - - - True - False - - - False - True - 4 - 19 - - - - - True - False - - - World - cbWorld - True - True - False - start - 70 - True - - - False - True - 0 - - - - - True - False - True - True - - - False - True - 4 - 1 - - - - - False - True - 20 - - - - - True - False - - - False - True - 4 - 21 - - - - - No pause - True - True - False - start - True - - - False - True - 22 - - - - - - - True - False - Advanced - - - - - - - - False - True - 1 - - - - - True - True - - - True - False - vertical - - - True - False - - - Server address - True - True - False - start - 13 - True - - - False - True - 0 - - - - - True - False - True - True - - - False - True - 4 - 1 - - - - - False - True - 0 - - - - - True - False - - - False - True - 4 - 1 - - - - - True - False - - - Server port - True - True - False - start - 37 - True - - - False - True - 0 - - - - - True - False - True - True - - - False - True - 4 - 1 - - - - - False - True - 2 - - - - - True - False - - - False - True - 4 - 3 - - - - - True - False - - - Server password - True - True - False - start - 2 - True - - - False - True - 0 - - - - - True - True - True - - - False - True - 4 - 1 - - - - - False - True - 4 - - - - - True - False - - - False - True - 4 - 5 - - - - - Host session - True - True - False - start - True - - - False - True - 6 - - - - - - - True - False - Client - - - - - - - - False - True - 2 - - - - - - - - - 1 - - - - - True - False - True - True - Parameters - - - 1 - False - - - - - True - False - vertical - - - Quit - True - True - True - - - False - True - 0 - - - - - - - - - - - 2 - - - - - True - False - Other - - - 2 - False - - - - - False - True - 0 - - - - - True - False - True - - - True - False - Mod Preset: - - - False - True - 0 - - - - - gtk-open - True - True - True - True - - - False - True - 1 - - - - - gtk-save - True - True - True - True - - - False - True - 2 - - - - - False - True - 1 - - - - - True - False - - - False - True - 2 - - - - - True - False - Selected 0 mods (0 from workshop, 0 custom) - - - False - True - 3 - - - - - True - False - end - False - True - bottom - - - gtk-media-play - True - True - True - True - True - - - False - True - 0 - - - - - True - False - True - True - Status: ArmA 3 not running - - - False - True - 1 - - - - - False - True - 4 - - - - - - diff --git a/MainWindow.cpp b/MainWindow.cpp deleted file mode 100644 index 5ee1af8..0000000 --- a/MainWindow.cpp +++ /dev/null @@ -1,1033 +0,0 @@ -/* - * MainWindow.cpp - * - * Created on: 29 Oct 2016 - * Author: muttley - */ - -#include "MainWindow.h" -#include -#include -#include "Settings.h" -#include "Filesystem.h" -#include "Logger.h" -#include "Utils.h" - -#include -#include - -MainWindow::MainWindow(BaseObjectType *cobject, const Glib::RefPtr &refGlade) : - Gtk::Window(cobject), builder(refGlade) -{ - Init(); - - dispatcher_.connect(sigc::mem_fun(*this, &MainWindow::notification_from_thread)); - - workshopModsStore = Glib::RefPtr::cast_dynamic(builder->get_object("workshopModsStore")); - customModsStore = Glib::RefPtr::cast_dynamic(builder->get_object("customModsStore")); - - //Mods tab - builder->get_widget("tvCustomMods", tvCustomMods); - builder->get_widget("tvWorkshopMods", tvWorkshopMods); - - builder->get_widget("btnAdd", btnAdd); - builder->get_widget("btnRemove", btnRemove); - - builder->get_widget("btnPresetLoad", btnPresetLoad); - builder->get_widget("btnPresetSave", btnPresetSave); - - workshopToggleBox = Glib::RefPtr::cast_dynamic(builder->get_object("workshopToggleBox")); - customToggleBox = Glib::RefPtr::cast_dynamic(builder->get_object("customToggleBox")); - - //Parameters tab - //Basic - builder->get_widget("cbSkipIntro", cbSkipIntro); - builder->get_widget("cbNosplash", cbNosplash); - builder->get_widget("cbWindow", cbWindow); - builder->get_widget("cbName", cbName); - builder->get_widget("tbName", tbName); - - //Advanced - builder->get_widget("cbParameterFile", cbParameterFile); - builder->get_widget("tbParameterFile", tbParameterFile); - builder->get_widget("btnParameterFileBrowse", btnParameterFileBrowse); - - builder->get_widget("cbCheckSignatures", cbCheckSignatures); - - builder->get_widget("cbCpuCount", cbCpuCount); - builder->get_widget("numCpuCount", numCpuCount); - - builder->get_widget("cbExThreads", cbExThreads); - builder->get_widget("cbExThreadsFileOperations", cbExThreadsFileOperations); - builder->get_widget("cbExThreadsTextureLoading", cbExThreadsTextureLoading); - builder->get_widget("cbExThreadsGeometryLoading", cbExThreadsGeometryLoading); - - builder->get_widget("cbEnableHT", cbEnableHT); - builder->get_widget("cbDisableMulticore", cbDisableMulticore); - builder->get_widget("cbHugePages", cbHugePages); - - builder->get_widget("cbFilePatching", cbFilePatching); - builder->get_widget("cbNoLogs", cbNoLogs); - builder->get_widget("cbShowScriptErrors", cbShowScriptErrors); - - builder->get_widget("cbWorld", cbWorld); - builder->get_widget("tbWorld", tbWorld); - - builder->get_widget("cbNoPause", cbNoPause); - - //Client - builder->get_widget("cbConnect", cbConnect); - builder->get_widget("tbConnect", tbConnect); - - builder->get_widget("cbPort", cbPort); - builder->get_widget("tbPort", tbPort); - - builder->get_widget("cbPassword", cbPassword); - builder->get_widget("tbPassword", tbPassword); - - builder->get_widget("cbHost", cbHost); - - //Other - builder->get_widget("btnQuit", btnQuit); - - //Visible everywhere - builder->get_widget("btnPlay", btnPlay); - - builder->get_widget("lblSelectedMods", lblSelectedMods); - builder->get_widget("lblStatus", lblStatus); - - //synchronize values with Settings - cbSkipIntro->set_active(Settings::SkipIntro); - cbNosplash->set_active(Settings::Nosplash); - cbWindow->set_active(Settings::Window); - cbName->set_active(Settings::Name); - tbName->set_text(Settings::NameValue); - - cbParameterFile->set_active(Settings::ParameterFile); - tbParameterFile->set_text(Settings::ParameterFileValue); - - cbCheckSignatures->set_active(Settings::CheckSignatures); - - cbCpuCount->set_active(Settings::CpuCount); - numCpuCount->set_value(Settings::CpuCountValue); - - cbExThreads->set_active(Settings::ExThreads); - cbExThreadsFileOperations->set_active(Settings::ExThreadsFileOperations); - cbExThreadsTextureLoading->set_active(Settings::ExThreadsTextureLoading); - cbExThreadsGeometryLoading->set_active(Settings::ExThreadsGeometryLoading); - - cbEnableHT->set_active(Settings::EnableHT); - cbDisableMulticore->set_active(Settings::DisableMulticore); - cbHugePages->set_active(Settings::HugePages); - - cbFilePatching->set_active(Settings::FilePatching); - cbNoLogs->set_active(Settings::NoLogs); - cbShowScriptErrors->set_active(Settings::ShowScriptErrors); - - cbWorld->set_active(Settings::World); - tbWorld->set_text(Settings::WorldValue); - - cbNoPause->set_active(Settings::NoPause); - - cbConnect->set_active(Settings::Connect); - tbConnect->set_text(Settings::ConnectValue); - - cbPort->set_active(Settings::Port); - tbPort->set_text(Settings::PortValue); - - cbPassword->set_active(Settings::Password); - tbPassword->set_text(Settings::PasswordValue); - - cbHost->set_active(Settings::Host); - - btnAdd->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::btnAdd_Clicked)); - btnRemove->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::btnRemove_Clicked)); - - btnPresetLoad->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::btnPresetLoad_Clicked)); - btnPresetSave->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::btnPresetSave_Clicked)); - - cbSkipIntro->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbSkipIntro_Toggled)); - cbNosplash->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbNosplash_Toggled)); - cbWindow->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbWindow_Toggled)); - cbName->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbName_Toggled)); - tbName->signal_changed().connect(sigc::mem_fun(*this, &MainWindow::tbName_Changed)); - - cbParameterFile->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbParameterFile_Toggled)); - tbParameterFile->signal_changed().connect(sigc::mem_fun(*this, &MainWindow::tbParameterFile_Changed)); - btnParameterFileBrowse->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::btnParameterFileBrowse_Clicked)); - - cbCheckSignatures->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbCheckSignatures_Toggled)); - - cbCpuCount->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbCpuCount_Toggled)); - numCpuCount->signal_changed().connect(sigc::mem_fun(*this, &MainWindow::numCpuCount_Changed)); - - cbExThreads->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbExThreads_Toggled)); - cbExThreadsFileOperations->signal_toggled().connect(sigc::mem_fun(*this, - &MainWindow::cbExThreadsFileOperations_Toggled)); - cbExThreadsTextureLoading->signal_toggled().connect(sigc::mem_fun(*this, - &MainWindow::cbExThreadsTextureLoading_Toggled)); - cbExThreadsGeometryLoading->signal_toggled().connect(sigc::mem_fun(*this, - &MainWindow::cbExThreadsGeometryLoading_Toggled)); - - cbEnableHT->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbEnableHT_Toggled)); - cbDisableMulticore->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbDisableMulticore_Toggled)); - cbHugePages->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbHugePages_Toggled)); - - cbFilePatching->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbFilePatching_Toggled)); - cbNoLogs->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbNoLogs_Toggled)); - cbShowScriptErrors->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbShowScriptErrors_Toggled)); - - cbWorld->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbWorld_Toggled)); - tbWorld->signal_changed().connect(sigc::mem_fun(*this, &MainWindow::tbWorld_Changed)); - - cbNoPause->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbNoPause_Toggled)); - - cbConnect->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbConnect_Toggled)); - tbConnect->signal_changed().connect(sigc::mem_fun(*this, &MainWindow::tbConnect_Changed)); - - cbPort->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbPort_Toggled)); - tbPort->signal_changed().connect(sigc::mem_fun(*this, &MainWindow::tbPort_Changed)); - - cbPassword->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbPassword_Toggled)); - tbPassword->signal_changed().connect(sigc::mem_fun(*this, &MainWindow::tbPassword_Changed)); - - cbHost->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::cbHost_Toggled)); - - btnQuit->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::btnQuit_Clicked)); - - btnPlay->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::btnPlay_Clicked)); - - cbNosplash->set_tooltip_text("(1.70) Game will Crash if you enable this and alt tab during the initial loading screen!" - "\nWait until you're in the main menu!"); - - /////Executing every event - need to make sure UI represents actual Settings - ignore = true; - cbSkipIntro_Toggled(); - cbNosplash_Toggled(); - cbWindow_Toggled(); - cbName_Toggled(); - tbName_Changed(); - cbParameterFile_Toggled(); - tbParameterFile_Changed(); - cbCheckSignatures_Toggled(); - cbCpuCount_Toggled(); - numCpuCount_Changed(); - - cbExThreads_Toggled(); - cbExThreadsFileOperations_Toggled(); - cbExThreadsTextureLoading_Toggled(); - cbExThreadsGeometryLoading_Toggled(); - cbEnableHT_Toggled(); - cbDisableMulticore_Toggled(); - cbHugePages_Toggled(); - cbFilePatching_Toggled(); - cbNoLogs_Toggled(); - cbShowScriptErrors_Toggled(); - cbWorld_Toggled(); - tbWorld_Changed(); - cbNoPause_Toggled(); - cbConnect_Toggled(); - tbConnect_Changed(); - cbPort_Toggled(); - tbPort_Changed(); - cbPassword_Toggled(); - tbPassword_Changed(); - cbHost_Toggled(); - ignore = false; - ///// - - set_title("ArmA 3 Unix Launcher"); - set_default_size(Settings::WindowSizeX, Settings::WindowSizeY); - move(Settings::WindowPosX, Settings::WindowPosY); - - for (std::string s : Settings::WorkshopModsOrder) - { - for (int i = 0; i < WorkshopMods.size(); i++) - { - if (WorkshopMods[i].WorkshopId == s) - { - Gtk::TreeModel::Row row = *(workshopModsStore.operator ->()->append()); - row[workshopColumns.enabled] = Settings::ModEnabled(WorkshopMods[i].WorkshopId); - row[workshopColumns.name] = WorkshopMods[i].Name; - row[workshopColumns.workshopid] = WorkshopMods[i].WorkshopId; - WorkshopMods.erase(WorkshopMods.begin() + i); - break; - } - } - } - - for (Mod m : WorkshopMods) - { - Gtk::TreeModel::Row row = *(workshopModsStore.operator ->()->append()); - row[workshopColumns.enabled] = Settings::ModEnabled(m.WorkshopId); - row[workshopColumns.name] = m.Name; - row[workshopColumns.workshopid] = m.WorkshopId; - } - - for (std::string s : Settings::CustomModsOrder) - { - for (int i = 0; i < CustomMods.size(); i++) - { - if (CustomMods[i].WorkshopId == s) - { - Gtk::TreeModel::Row row = *(customModsStore.operator ->()->append()); - row[customColumns.enabled] = Settings::ModEnabled(CustomMods[i].Path); - row[customColumns.name] = CustomMods[i].Name; - row[customColumns.path] = Utils::Replace(CustomMods[i].Path, Settings::ArmaPath, Filesystem::ArmaDirMark); - CustomMods.erase(CustomMods.begin() + i); - break; - } - } - } - - for (Mod m : CustomMods) - { - Gtk::TreeModel::Row row = *(customModsStore.operator ->()->append()); - row[customColumns.enabled] = Settings::ModEnabled(m.Path); - row[customColumns.name] = m.Name; - row[customColumns.path] = Utils::Replace(m.Path, Settings::ArmaPath, Filesystem::ArmaDirMark); - } - - workshopToggleBox->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::WorkshopToggleBox_Toggled)); - customToggleBox->signal_toggled().connect(sigc::mem_fun(*this, &MainWindow::CustomToggleBox_Toggled)); - - this->signal_delete_event().connect(sigc::mem_fun(*this, &MainWindow::onExit)); - - RefreshStatusLabel(); - - stop_arma_thread.store(false); - armaStatusThread = new std::thread(&MainWindow::ArmaStatusThread, this); - sleep(1); // sleep - wait for armaStatusThread to update - - if (!Settings::PresetToRun.empty()) - { - if (!Filesystem::FileExists(Settings::PresetToRun)) - { - std::string path = Filesystem::HomeDirectory + Filesystem::LauncherSettingsDirectory; - std::string newPresetPath = path + "/" + Settings::PresetToRun; - if (Filesystem::FileExists(newPresetPath)) - Settings::PresetToRun = newPresetPath; - else - Settings::PresetToRun = newPresetPath + ".a3ulm"; - } - if (!Filesystem::FileExists(Settings::PresetToRun)) - { - LOG(1, "Preset file does not exist, exiting..."); - exit(1); - } - load_preset(Settings::PresetToRun); - btnPlay_Clicked(); - exit(0); - } -} - -void MainWindow::notify() -{ - dispatcher_.emit(); -} - -void MainWindow::notification_from_thread() -{ - pid_t pid = armaPid; - if (pid != -1) - lblStatus->set_text("Status: ArmA 3 running, PID: " + std::to_string(armaPid)); - else - lblStatus->set_text("Status: ArmA 3 not running"); -} - -void MainWindow::ArmaStatusThread() -{ - LOG(1, "Status monitoring thread started"); - while (!stop_arma_thread.load()) - { - #ifdef __APPLE__ - armaPid = Utils::FindProcess("ArmA3"); - #else - armaPid = Utils::FindProcess("./arma3.x86_64"); - if (armaPid == -1) - armaPid = Utils::FindProcess("Arma3_x64.exe"); - #endif - this->notify(); - std::this_thread::sleep_for(std::chrono::seconds(2)); - } -} - -void MainWindow::btnAdd_Clicked() -{ - Gtk::FileChooserDialog fcDialog(*this, "Select new mod folder", Gtk::FILE_CHOOSER_ACTION_SELECT_FOLDER); - /*fcDialog.set_title("Select new mod folder"); - fcDialog.set_action(Gtk::FILE_CHOOSER_ACTION_SELECT_FOLDER);*/ - fcDialog.add_button("_Open", 1); - fcDialog.add_button("_Cancel", 0); - int result = fcDialog.run(); - - LOG(0, "Add dialog result: " + std::to_string(result)); - if (result) - { - LOG(0, "Selected filename: " + fcDialog.get_filename()); - LOG(0, "Current folder: " + fcDialog.get_current_folder()); - std::string selectedPath = fcDialog.get_filename(); - if (!Utils::ContainsAddons(selectedPath)) - selectedPath = fcDialog.get_current_folder(); - - if (selectedPath == Settings::ArmaPath) - { - Gtk::MessageDialog msgDialog("You can't add ArmA's main directory", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); - msgDialog.run(); - LOG(1, "Selected directory equals ArmA path"); - return; - } - - if (!Utils::ContainsAddons(selectedPath)) - { - Gtk::MessageDialog msgDialog("Couldn't find Addons folder in selected directory", false, Gtk::MESSAGE_ERROR, - Gtk::BUTTONS_OK, true); - msgDialog.run(); - LOG(1, "Selected directory doesn't contain Addons folder"); - return; - } - - for (int i = 0; i < customModsStore.operator ->()->children().size(); i++) - { - Gtk::TreeModel::Row row = *(customModsStore.operator ->()->get_iter(std::to_string(i).c_str())); - - Glib::ustring path = row[customColumns.path]; - std::string pathStr = Utils::Replace(path.raw(), Filesystem::ArmaDirMark, Settings::ArmaPath); - - if (pathStr == selectedPath) - { - Gtk::MessageDialog msgDialog("Mod with this path exists", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); - msgDialog.run(); - LOG(1, "Selected directory exists in customModsStore"); - return; - } - } - - Gtk::TreeModel::Row row = *(customModsStore.operator ->()->append()); - Mod m(selectedPath, "-1"); - row[customColumns.enabled] = false; - row[customColumns.name] = m.Name; - row[customColumns.path] = Utils::Replace(m.Path, Settings::ArmaPath, Filesystem::ArmaDirMark); - - std::vector modList; - modList.push_back(m); - - Filesystem::CheckFileStructure(Settings::ArmaPath, Settings::WorkshopPath, modList); - } - RefreshStatusLabel(); -} - -void MainWindow::btnRemove_Clicked() -{ - Glib::RefPtr treeSel = tvCustomMods->get_selection(); - if (!treeSel.operator ->()->get_selected()) - { - Gtk::MessageDialog msgDialog("Select a mod to delete first", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); - msgDialog.run(); - return; - } - Gtk::TreeModel::Row row = *(treeSel.operator ->()->get_selected()); - Glib::ustring path = row[customColumns.path]; - if (path.raw().find(Filesystem::ArmaDirMark) != std::string::npos) - { - Gtk::MessageDialog msgDialog("You can't remove mods from ArmA's directory", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, - true); - msgDialog.run(); - LOG(1, "Can't delete mods from ArmA's directory"); - return; - } - - customModsStore.operator ->()->erase(treeSel.operator ->()->get_selected()); - RefreshStatusLabel(); -} - -void MainWindow::load_preset(std::string filename) -{ - Settings::ModPreset = filename.substr(filename.find_last_of('/') + 1); - std::string contents = Filesystem::ReadAllText(filename); - Settings::WorkshopModsEnabled.clear(); - Settings::CustomModsEnabled.clear(); - - //load mods into settings - for (std::string line : Utils::Split(contents, "\n")) - { - if (Utils::StartsWith(line, "WorkshopModsEnabled=")) - { - std::string sub = line.substr(20); - for (std::string s : Utils::Split(sub, ",")) - Settings::WorkshopModsEnabled.push_back(s); - } - else if (Utils::StartsWith(line, "CustomModsEnabled=")) - { - std::string sub = line.substr(18); - for (std::string s : Utils::Split(sub, ",")) - Settings::CustomModsEnabled.push_back(s); - } - } - - //update UI to reflect changes - for (int i = 0; i < workshopModsStore.operator ->()->children().size(); i++) - { - Gtk::TreeModel::Row row = *(workshopModsStore.operator ->()->get_iter(std::to_string(i).c_str())); - - Glib::ustring workshopid = row[workshopColumns.workshopid]; - - row[workshopColumns.enabled] = Settings::ModEnabled(workshopid.raw()); - } - - for (int i = 0; i < customModsStore.operator ->()->children().size(); i++) - { - Gtk::TreeModel::Row row = *(customModsStore.operator ->()->get_iter(std::to_string(i).c_str())); - - Glib::ustring path = row[customColumns.path]; - std::string pathStr = Utils::Replace(path.raw(), Filesystem::ArmaDirMark, Settings::ArmaPath); - - row[customColumns.enabled] = Settings::ModEnabled(pathStr); - } - - RefreshStatusLabel(); -} - -void MainWindow::btnPresetLoad_Clicked() -{ - Gtk::FileChooserDialog fcDialog(*this, "Select a Preset", Gtk::FILE_CHOOSER_ACTION_OPEN); - fcDialog.add_button("_Open", 1); - fcDialog.add_button("_Cancel", 0); - fcDialog.set_current_folder(Filesystem::HomeDirectory + Filesystem::LauncherSettingsDirectory); - - Glib::RefPtr fcFilter = Gtk::FileFilter::create(); - fcFilter->set_name("Preset files (*.a3ulm)"); - fcFilter->add_pattern("*.a3ulm"); - fcDialog.add_filter(fcFilter); - - Glib::RefPtr fcFilterAll = Gtk::FileFilter::create(); - fcFilterAll->set_name("All files (*)"); - fcFilterAll->add_pattern("*"); - fcDialog.add_filter(fcFilterAll); - - int result = fcDialog.run(); - - if (result) - load_preset(fcDialog.get_filename()); -} - -void MainWindow::btnPresetSave_Clicked() -{ - PutModsToSettings(); - - Gtk::FileChooserDialog fcDialog(*this, "Name your Preset", Gtk::FILE_CHOOSER_ACTION_SAVE); - fcDialog.add_button("_Save", 1); - fcDialog.add_button("_Cancel", 0); - std::string path = Filesystem::HomeDirectory + Filesystem::LauncherSettingsDirectory; - fcDialog.set_current_folder(path); - - Glib::RefPtr fcFilter = Gtk::FileFilter::create(); - fcFilter->set_name("Preset files (*.a3ulm)"); - fcFilter->add_pattern("*.a3ulm"); - fcDialog.add_filter(fcFilter); - - int result = fcDialog.run(); - - if (result) - { - std::string fname = fcDialog.get_filename(); - // Check if file name ends with .a3ulm, if not, then append - if (!Utils::EndsWith(fname, ".a3ulm")) - fname += ".a3ulm"; - Settings::ModPreset = fname.substr(fname.find_last_of('/') + 1); - RefreshStatusLabel(); - - std::string outfile = "WorkshopModsEnabled="; - for (std::string s : Settings::WorkshopModsEnabled) - outfile += s + ","; - outfile += "\nCustomModsEnabled="; - for (std::string s : Settings::CustomModsEnabled) - outfile += Utils::Replace(s, Filesystem::ArmaDirMark, Settings::ArmaPath) + ","; - - Filesystem::WriteAllText(fname, outfile); - } -} - -bool MainWindow::onExit(GdkEventAny *event) -{ - LOG(1, "onExit()"); - - this->get_position(Settings::WindowPosX, Settings::WindowPosY); - this->get_size(Settings::WindowSizeX, Settings::WindowSizeY); - - PutModsToSettings(); - - Settings::Save(Filesystem::HomeDirectory - + Filesystem::LauncherSettingsDirectory - + Filesystem::LauncherSettingsFilename); - - stop_arma_thread.store(true); - armaStatusThread->join(); - return false; -} - -void MainWindow::WorkshopToggleBox_Toggled(Glib::ustring path) //path is index number -{ - //std::cout << path << std::endl; - Gtk::TreeModel::Row row = *(workshopModsStore.operator ->()->get_iter(path)); - row[workshopColumns.enabled] = !row[workshopColumns.enabled]; - if (Settings::ModPreset[Settings::ModPreset.length() - 1] != '*') - Settings::ModPreset += "*"; - RefreshStatusLabel(); -} - -void MainWindow::CustomToggleBox_Toggled(Glib::ustring path) //path is index number -{ - //std::cout << path << std::endl; - Gtk::TreeModel::Row row = *(customModsStore.operator ->()->get_iter(path)); - row[customColumns.enabled] = !row[customColumns.enabled]; - if (Settings::ModPreset[Settings::ModPreset.length() - 1] != '*') - Settings::ModPreset += "*"; - RefreshStatusLabel(); -} - -void MainWindow::cbSkipIntro_Toggled() -{ - LOG(0, "cbSkipIntro_Toggled: " + Utils::ToString(cbSkipIntro->get_active())); - if (!ignore) Settings::SkipIntro = cbSkipIntro->get_active(); -} - -void MainWindow::cbNosplash_Toggled() -{ - LOG(0, "cbNosplash_Toggled: " + Utils::ToString(cbNosplash->get_active())); - if (!ignore) Settings::Nosplash = cbNosplash->get_active(); -} - -void MainWindow::cbWindow_Toggled() -{ - LOG(0, "cbWindow_Toggled: " + Utils::ToString(cbWindow->get_active())); - if (!ignore) Settings::Window = cbWindow->get_active(); -} - -void MainWindow::cbName_Toggled() -{ - LOG(0, "cbName_Toggled: " + Utils::ToString(cbName->get_active())); - if (!ignore) Settings::Name = cbName->get_active(); - tbName->set_sensitive(Settings::Name); -} - -void MainWindow::tbName_Changed() -{ - LOG(0, "tbName_Changed: " + tbName->get_text()); - if (!ignore) Settings::NameValue = tbName->get_text(); -} - -void MainWindow::cbParameterFile_Toggled() -{ - LOG(0, "cbParameterFile_Toggled: " + Utils::ToString(cbParameterFile->get_active())); - if (!ignore) Settings::ParameterFile = cbParameterFile->get_active(); - tbParameterFile->set_sensitive(Settings::ParameterFile); - btnParameterFileBrowse->set_sensitive(Settings::ParameterFile); -} - -void MainWindow::tbParameterFile_Changed() -{ - LOG(0, "tbParameterFile_Changed: " + tbParameterFile->get_text()); - if (!ignore) Settings::ParameterFileValue = tbParameterFile->get_text(); -} - -void MainWindow::btnParameterFileBrowse_Clicked() -{ - LOG(0, "btnParameterFileBrowse_Clicked"); - Gtk::FileChooserDialog fcDialog(*this, "Select parameter file", Gtk::FILE_CHOOSER_ACTION_OPEN); - fcDialog.add_button("_Open", 1); - fcDialog.add_button("_Cancel", 0); - int result = fcDialog.run(); - - if (result) - tbParameterFile->set_text(fcDialog.get_filename()); -} - -void MainWindow::cbCheckSignatures_Toggled() -{ - LOG(0, "cbCheckSignatures_Toggled: " + Utils::ToString(cbCheckSignatures->get_active())); - if (!ignore) Settings::CheckSignatures = cbCheckSignatures->get_active(); -} - -void MainWindow::cbCpuCount_Toggled() -{ - LOG(0, "cbCpuCount_Toggled: " + Utils::ToString(cbCpuCount->get_active())); - if (!ignore) Settings::CpuCount = cbCpuCount->get_active(); - numCpuCount->set_sensitive(Settings::CpuCount); -} - -void MainWindow::numCpuCount_Changed() -{ - LOG(0, "numCpuCount_Changed: " + std::to_string(numCpuCount->get_value())); - if (!ignore) Settings::CpuCountValue = numCpuCount->get_value(); -} - -void MainWindow::cbExThreads_Toggled() -{ - LOG(0, "cbExThreads_Toggled: " + Utils::ToString(cbExThreads->get_active())); - if (!ignore) Settings::ExThreads = cbExThreads->get_active(); - cbExThreadsFileOperations->set_sensitive(Settings::ExThreads); - cbExThreadsTextureLoading->set_sensitive(Settings::ExThreads); - cbExThreadsGeometryLoading->set_sensitive(Settings::ExThreads); -} - -void MainWindow::cbExThreadsFileOperations_Toggled() -{ - LOG(0, "cbExThreadsFileOperations_Toggled: " + Utils::ToString(cbExThreadsFileOperations->get_active())); - if (!ignore) - { - Settings::ExThreadsFileOperations = cbExThreadsFileOperations->get_active(); - if (!Settings::ExThreadsFileOperations) - { - cbExThreadsTextureLoading->set_active(false); - cbExThreadsGeometryLoading->set_active(false); - Settings::ExThreadsGeometryLoading = false; - Settings::ExThreadsTextureLoading = false; - } - } -} - -void MainWindow::cbExThreadsTextureLoading_Toggled() -{ - LOG(0, "cbExThreadsTextureLoading_Toggled: " + Utils::ToString(cbExThreadsTextureLoading->get_active())); - if (!ignore) - { - Settings::ExThreadsTextureLoading = cbExThreadsTextureLoading->get_active(); - if (Settings::ExThreadsTextureLoading) - { - cbExThreadsFileOperations->set_active(true); - Settings::ExThreadsFileOperations = true; - } - } -} - -void MainWindow::cbExThreadsGeometryLoading_Toggled() -{ - LOG(0, "cbExThreadsGeometryLoading_Toggled: " + Utils::ToString(cbExThreadsGeometryLoading->get_active())); - if (!ignore) - { - Settings::ExThreadsGeometryLoading = cbExThreadsGeometryLoading->get_active(); - if (Settings::ExThreadsGeometryLoading) - { - cbExThreadsFileOperations->set_active(true); - Settings::ExThreadsFileOperations = true; - } - } -} - -void MainWindow::cbEnableHT_Toggled() -{ - LOG(0, "cbEnableHT_Toggled: " + Utils::ToString(cbEnableHT->get_active())); - if (!ignore) Settings::EnableHT = cbEnableHT->get_active(); -} - -void MainWindow::cbDisableMulticore_Toggled() -{ - LOG(0, "cbDisableMulticore_Toggled: " + Utils::ToString(cbDisableMulticore->get_active())); - if (!ignore) Settings::DisableMulticore = cbDisableMulticore->get_active(); -} - -void MainWindow::cbHugePages_Toggled() -{ - LOG(0, "cbHugePages_Toggled: " + Utils::ToString(cbHugePages->get_active())); - if (!ignore) Settings::HugePages = cbHugePages->get_active(); -} - -void MainWindow::cbFilePatching_Toggled() -{ - LOG(0, "cbFilePatching_Toggled: " + Utils::ToString(cbFilePatching->get_active())); - if (!ignore) Settings::FilePatching = cbFilePatching->get_active(); -} - -void MainWindow::cbNoLogs_Toggled() -{ - LOG(0, "cbNoLogs_Toggled: " + Utils::ToString(cbNoLogs->get_active())); - if (!ignore) Settings::NoLogs = cbNoLogs->get_active(); -} - -void MainWindow::cbShowScriptErrors_Toggled() -{ - LOG(0, "cbShowScriptErrors_Toggled: " + Utils::ToString(cbShowScriptErrors->get_active())); - if (!ignore) Settings::ShowScriptErrors = cbShowScriptErrors->get_active(); -} - -void MainWindow::cbWorld_Toggled() -{ - LOG(0, "cbWorld_Toggled: " + Utils::ToString(cbWorld->get_active())); - if (!ignore) Settings::World = cbWorld->get_active(); - tbWorld->set_sensitive(Settings::World); -} - -void MainWindow::tbWorld_Changed() -{ - LOG(0, "tbWorld_Changed: " + tbWorld->get_text()); - if (!ignore) Settings::WorldValue = tbWorld->get_text(); -} - -void MainWindow::cbNoPause_Toggled() -{ - LOG(0, "cbNoPause_Toggled: " + Utils::ToString(cbNoPause->get_active())); - if (!ignore) Settings::NoPause = cbNoPause->get_active(); -} - -void MainWindow::cbConnect_Toggled() -{ - LOG(0, "cbConnect_Toggled: " + Utils::ToString(cbConnect->get_active())); - if (!ignore) Settings::Connect = cbConnect->get_active(); - tbConnect->set_sensitive(Settings::Connect); -} - -void MainWindow::tbConnect_Changed() -{ - LOG(0, "tbConnect_Changed: " + tbConnect->get_text()); - if (!ignore) Settings::ConnectValue = tbConnect->get_text(); -} - -void MainWindow::cbPort_Toggled() -{ - LOG(0, "cbPort_Toggled: " + Utils::ToString(cbPort->get_active())); - if (!ignore) Settings::Port = cbPort->get_active(); - tbPort->set_sensitive(Settings::Port); -} - -void MainWindow::tbPort_Changed() -{ - LOG(0, "tbPort_Changed: " + tbPort->get_text()); - if (!ignore) Settings::PortValue = tbPort->get_text(); -} - -void MainWindow::cbPassword_Toggled() -{ - LOG(0, "cbPassword_Toggled: " + Utils::ToString(cbPassword->get_active())); - if (!ignore) Settings::Password = cbPassword->get_active(); - tbPassword->set_sensitive(Settings::Password); -} - -void MainWindow::tbPassword_Changed() -{ - LOG(0, "tbPassword_Changed: " + tbPassword->get_text()); - if (!ignore) Settings::PasswordValue = tbPassword->get_text(); -} - -void MainWindow::cbHost_Toggled() -{ - LOG(0, "cbHost_Toggled: " + Utils::ToString(cbHost->get_active())); - if (!ignore) Settings::Host = cbHost->get_active(); -} - -void MainWindow::btnQuit_Clicked() -{ - this->close(); -} - -void MainWindow::btnPlay_Clicked() -{ - LOG(0, "btnPlay_Clicked"); - - if (armaPid != -1) - { - Gtk::MessageDialog msg("ArmA 3 is already running! PID: " + std::to_string(armaPid), false, Gtk::MESSAGE_INFO, - Gtk::BUTTONS_OK, true); - msg.run(); - return; - } - - std::string parameters; - - PutModsToSettings(); - - std::vector modList; - - LOG(1, "FullModList size:" + std::to_string(FullModList.size())); - - for (std::string s : Settings::WorkshopModsEnabled) - { - for (int i = 0; i < FullModList.size(); i++) - { - if (FullModList[i].WorkshopId == s) - { - LOG(0, "Mod: " + FullModList[i].WorkshopId); - modList.push_back(&FullModList[i]); - break; - } - } - } - - for (std::string s : Settings::CustomModsEnabled) - { - for (int i = 0; i < FullModList.size(); i++) - { - if (FullModList[i].Path == s) - { - LOG(0, "Mod: " + FullModList[i].Path); - modList.push_back(&FullModList[i]); - break; - } - } - } - - LOG(1, Filesystem::ArmaConfigFile); - std::string newArmaCfg = Filesystem::GenerateArmaCfg(Settings::ArmaPath, Filesystem::ArmaConfigFile, modList); - Filesystem::WriteAllText(Filesystem::ArmaConfigFile, newArmaCfg); - LOG(0, "Arma3.cfg:\n--------------------\n" + newArmaCfg + "\n--------------------"); - - parameters += Settings::SkipIntro ? "-skipIntro " : ""; - parameters += Settings::Nosplash ? "-noSplash " : ""; - parameters += Settings::Window ? "-window " : ""; - parameters += Settings::Name ? "-name=" + Settings::NameValue + " " : ""; - - parameters += Settings::ParameterFile ? "-par=" + Settings::ParameterFileValue + " " : ""; - - parameters += Settings::CheckSignatures ? "-checkSignatures " : ""; - - parameters += Settings::CpuCount ? "-cpuCount=" + std::to_string(Settings::CpuCountValue) + " " : ""; - - int exThreadsValue = Settings::ExThreadsFileOperations - + Settings::ExThreadsTextureLoading * 2 - + Settings::ExThreadsGeometryLoading * 4; - - parameters += Settings::ExThreads ? "-exThreads=" + std::to_string(exThreadsValue) + " " : ""; - - parameters += Settings::EnableHT ? "-enableHT " : ""; - parameters += Settings::DisableMulticore ? "-noCB " : ""; - parameters += Settings::HugePages ? "-hugePages " : ""; - - parameters += Settings::FilePatching ? "-filePatching " : ""; - parameters += Settings::NoLogs ? "-noLogs " : ""; - parameters += Settings::ShowScriptErrors ? "-showScriptErrors " : ""; - - parameters += Settings::World ? "-world=" + Settings::WorldValue + " " : ""; - - parameters += Settings::NoPause ? "-noPause " : ""; - - parameters += Settings::Connect ? "-connect=" + Settings::ConnectValue + " " : ""; - parameters += Settings::Port ? "-port=" + Settings::PortValue + " " : ""; - parameters += Settings::Password ? "-password=" + Settings::PasswordValue + " " : ""; - - parameters += Settings::Host ? "-host" : ""; - - std::string launch_command; - #ifdef __APPLE__ - launch_command = "open steam://run/107410//" + Utils::Replace(parameters, " ", "%20"); - #else - if (Filesystem::IsProton(Settings::ArmaPath)) - launch_command = "steam -applaunch 107410 -nolauncher " + parameters; - else - launch_command = "steam -applaunch 107410 " + parameters; - #endif - LOG(0, "Arma 3 launch command:\n steam -applaunch 107410 " + parameters); - Glib::spawn_command_line_async(launch_command); -} - -void MainWindow::RefreshStatusLabel() -{ - int workshopMods = 0, customMods = 0; - for (int i = 0; i < workshopModsStore.operator ->()->children().size(); i++) - { - Gtk::TreeModel::Row row = *(workshopModsStore.operator ->()->get_iter(std::to_string(i).c_str())); - if (row[workshopColumns.enabled]) - workshopMods++; - } - for (int i = 0; i < customModsStore.operator ->()->children().size(); i++) - { - Gtk::TreeModel::Row row = *(customModsStore.operator ->()->get_iter(std::to_string(i).c_str())); - if (row[customColumns.enabled]) - customMods++; - } - - lblSelectedMods->set_text("Selected " + std::to_string(workshopMods + customMods) - + " mods (" + std::to_string(workshopMods) - + " from workshop, " + std::to_string(customMods) - + " custom)" - + "\t" - + "Mod Preset: " + Settings::ModPreset); -} - -void MainWindow::Init() -{ - LOG(1, "ArmA 3 Path: " + Settings::ArmaPath + "\nWorkshop mods path: " + Settings::WorkshopPath); - - WorkshopMods = Filesystem::FindMods(Settings::WorkshopPath); - - for (std::string path : Settings::CustomModsOrder) - { - LOG(0, "Custom mod: " + path); - //path = Utils::Replace(path, Filesystem::ArmaDirMark, Settings::ArmaPath); - CustomMods.push_back(Mod(path, "-1")); - } - - std::vector ArmaDirMods = Filesystem::FindMods(Settings::ArmaPath); - for (Mod m : ArmaDirMods) - { - bool alreadyExists = false; - for (Mod n : CustomMods) - { - if (n.Path == m.Path) - alreadyExists = true; - } - if (!alreadyExists) - CustomMods.push_back(m); - } - - // Removes CustomMods that are not located inside the ArmaDir anymore. - for (std::vector::iterator it = CustomMods.begin(); it != CustomMods.end();) - { - bool found = false; - for (Mod m : ArmaDirMods) - { - if (it->Path == m.Path) - { - found = true; - break; - } - } - if (!found) - it = CustomMods.erase(it); - else - it++; - } - - FullModList.clear(); - for (Mod m : WorkshopMods) - FullModList.push_back(m); - for (Mod m : CustomMods) - FullModList.push_back(m); - - Filesystem::CheckFileStructure(Settings::ArmaPath, Settings::WorkshopPath, FullModList); -} - -void MainWindow::PutModsToSettings() -{ - Settings::WorkshopModsEnabled.clear(); - Settings::WorkshopModsOrder.clear(); - - Settings::CustomModsEnabled.clear(); - Settings::CustomModsOrder.clear(); - - for (int i = 0; i < workshopModsStore.operator ->()->children().size(); i++) - { - Gtk::TreeModel::Row row = *(workshopModsStore.operator ->()->get_iter(std::to_string(i).c_str())); - LOG(0, "[W" + Utils::ToString(row[workshopColumns.enabled]) + "] Name: " + row[workshopColumns.name] + " WorkshopId: " + - row[workshopColumns.workshopid]); - - Glib::ustring workshopId = row[workshopColumns.workshopid]; - Settings::WorkshopModsOrder.push_back(workshopId); - - if (row[workshopColumns.enabled]) - Settings::WorkshopModsEnabled.push_back(workshopId.raw()); - } - - for (int i = 0; i < customModsStore.operator ->()->children().size(); i++) - { - Gtk::TreeModel::Row row = *(customModsStore.operator ->()->get_iter(std::to_string(i).c_str())); - - Glib::ustring path = row[customColumns.path]; - std::string pathStr = Utils::Replace(path.raw(), Filesystem::ArmaDirMark, Settings::ArmaPath); - - LOG(0, "[C" + Utils::ToString(row[customColumns.enabled]) + "] Name: " + row[customColumns.name] + " Path: " + pathStr); - Settings::CustomModsOrder.push_back(pathStr); - - if (row[customColumns.enabled]) - Settings::CustomModsEnabled.push_back(pathStr); - } -} diff --git a/MainWindow.h b/MainWindow.h deleted file mode 100644 index 01e40da..0000000 --- a/MainWindow.h +++ /dev/null @@ -1,215 +0,0 @@ -/* - * MainWindow.h - * - * Created on: 29 Oct 2016 - * Author: muttley - */ - -#ifndef MAINWINDOW_H_ -#define MAINWINDOW_H_ - -#include -#include -#include "Mod.h" -#include -#include - -class MainWindow : public Gtk::Window -{ - protected: - Glib::RefPtr builder; - - Glib::RefPtr workshopModsStore; - Glib::RefPtr customModsStore; - - //Mods tab - Gtk::TreeView *tvWorkshopMods; - Gtk::TreeView *tvCustomMods; - - Gtk::Button *btnAdd; - Gtk::Button *btnRemove; - - Gtk::Button *btnPresetLoad; - Gtk::Button *btnPresetSave; - - Glib::RefPtr workshopToggleBox; - Glib::RefPtr customToggleBox; - - //Parameters tab - - //Basic - Gtk::CheckButton *cbSkipIntro; - Gtk::CheckButton *cbNosplash; - Gtk::CheckButton *cbWindow; - Gtk::CheckButton *cbName; - Gtk::Entry *tbName; - - //Advanced - Gtk::CheckButton *cbParameterFile; - Gtk::Entry *tbParameterFile; - Gtk::Button *btnParameterFileBrowse; - - Gtk::CheckButton *cbCheckSignatures; - - Gtk::CheckButton *cbCpuCount; - Gtk::SpinButton *numCpuCount; - - Gtk::CheckButton *cbExThreads; - Gtk::CheckButton *cbExThreadsFileOperations; - Gtk::CheckButton *cbExThreadsTextureLoading; - Gtk::CheckButton *cbExThreadsGeometryLoading; - - Gtk::CheckButton *cbEnableHT; - Gtk::CheckButton *cbDisableMulticore; - Gtk::CheckButton *cbHugePages; - - Gtk::CheckButton *cbFilePatching; - Gtk::CheckButton *cbNoLogs; - Gtk::CheckButton *cbShowScriptErrors; - - Gtk::CheckButton *cbWorld; - Gtk::Entry *tbWorld; - - Gtk::CheckButton *cbNoPause; - - //Client - Gtk::CheckButton *cbConnect; - Gtk::Entry *tbConnect; - - Gtk::CheckButton *cbPort; - Gtk::Entry *tbPort; - - Gtk::CheckButton *cbPassword; - Gtk::Entry *tbPassword; - - Gtk::CheckButton *cbHost; - - //Other - Gtk::Button *btnQuit; - - //Visible everywhere - Gtk::Button *btnPlay; - - Gtk::Label *lblSelectedMods; - Gtk::Label *lblStatus; - - class WorkshopModelColumns : public Gtk::TreeModel::ColumnRecord - { - public: - WorkshopModelColumns() - { - add(enabled); - add(name); - add(workshopid); - } - - Gtk::TreeModelColumn enabled; - Gtk::TreeModelColumn name; - Gtk::TreeModelColumn workshopid; - }; - - class CustomModelColumns : public Gtk::TreeModel::ColumnRecord - { - public: - CustomModelColumns() - { - add(enabled); - add(name); - add(path); - } - - Gtk::TreeModelColumn enabled; - Gtk::TreeModelColumn name; - Gtk::TreeModelColumn path; - }; - - WorkshopModelColumns workshopColumns; - CustomModelColumns customColumns; - - std::vector WorkshopMods; - std::vector CustomMods; - - public: - MainWindow(BaseObjectType *cobject, const Glib::RefPtr &refGlade); - - void notify(); - - protected: - bool ignore; - std::atomic armaPid; - - std::vector FullModList; - - std::atomic stop_arma_thread; - std::thread *armaStatusThread; - Glib::Dispatcher dispatcher_; - void notification_from_thread(); - - void ArmaStatusThread(); - - void btnAdd_Clicked(); - void btnRemove_Clicked(); - - void load_preset(std::string path); - void btnPresetLoad_Clicked(); - void btnPresetSave_Clicked(); - - bool onExit(GdkEventAny *event); - void WorkshopToggleBox_Toggled(Glib::ustring path); - void CustomToggleBox_Toggled(Glib::ustring path); - - void cbSkipIntro_Toggled(); - void cbNosplash_Toggled(); - void cbWindow_Toggled(); - void cbName_Toggled(); - void tbName_Changed(); - - void cbParameterFile_Toggled(); - void tbParameterFile_Changed(); - void btnParameterFileBrowse_Clicked(); - - void cbCheckSignatures_Toggled(); - - void cbCpuCount_Toggled(); - void numCpuCount_Changed(); - - void cbExThreads_Toggled(); - void cbExThreadsFileOperations_Toggled(); - void cbExThreadsTextureLoading_Toggled(); - void cbExThreadsGeometryLoading_Toggled(); - - void cbEnableHT_Toggled(); - void cbDisableMulticore_Toggled(); - void cbHugePages_Toggled(); - - void cbFilePatching_Toggled(); - void cbNoLogs_Toggled(); - void cbShowScriptErrors_Toggled(); - - void cbWorld_Toggled(); - void tbWorld_Changed(); - - void cbNoPause_Toggled(); - - void cbConnect_Toggled(); - void tbConnect_Changed(); - - void cbPort_Toggled(); - void tbPort_Changed(); - - void cbPassword_Toggled(); - void tbPassword_Changed(); - - void cbHost_Toggled(); - - void btnQuit_Clicked(); - - void btnPlay_Clicked(); - - void RefreshStatusLabel(); - - void Init(); - void PutModsToSettings(); -}; - -#endif /* MAINWINDOW_H_ */ diff --git a/Mod.cpp b/Mod.cpp deleted file mode 100644 index f63c153..0000000 --- a/Mod.cpp +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Mod.cpp - * - * Created on: 18 Oct 2016 - * Author: muttley - */ - -#include "Mod.h" - -#include -#include -#include - -#include "Utils.h" -#include "Filesystem.h" -#include "Settings.h" -#include "Logger.h" - -using namespace std; - -// workshopId = -1 -> mod is not from workshop -// workshopId > 1 -> mod from workshop, workshop -Mod::Mod(string path, string workshopId) -{ - Path = path; - Name = DirName = Picture = LogoSmall = Logo = LogoOver = Action = TooltipOwned = Overview = ""; - DlcColor = {-1, -1, -1, -1}; - HideName = HidePicture = false; - PublishedId = "-1"; - IsRepresentedBySymlink = false; - - Enabled = false; - - WorkshopId = workshopId; - - string metaPath = path + "/meta.cpp", modPath = path + "/mod.cpp"; - if (!Filesystem::FileExists(metaPath)) - metaPath = ""; - if (!Filesystem::FileExists(modPath)) - modPath = ""; - ParseCPP(metaPath, modPath, path, workshopId); - - //Mod is added manually by user and din't contain any data in meta.cpp or mod.cpp - //quick & dirty -> Name = DirectoryName - if (Name == "" || DirName == "") - { - int withoutLastElement = Utils::RemoveLastElement(path, false).size(); - if (Name == "") - Name = path.substr(withoutLastElement); - if (DirName == "") - DirName = path.substr(withoutLastElement); - } -} - -Mod::~Mod() -{ -} - -string Mod::ParseString(string input) -{ - string response = input; - - regex removeComments("(/\\*.*?\\*/|//[^\\r\\n]*$)"); - regex removeWhitespaces("\\s+(?=([^\"]*\"[^\"]*\")*[^\"]*$)"); - - response = regex_replace(response, removeComments, ""); - response = regex_replace(response, removeWhitespaces, ""); - - return response; -} - -//As anyone would expect - documentation on this is trash -void Mod::ParseCPP(string meta, string mod, string path, string workshopId) -{ - if (meta != "") - { - string file = ParseString(Filesystem::ReadAllText(meta)); - //cout << "Meta.cpp: "<< file << endl; - vector instructions = Utils::Split(file, ";"); - for (string s : instructions) - { - if (Utils::StartsWith(s, "name")) - { - //name="hello" -> hello - this->Name = s.substr(6, s.size() - 7); - } - else if (Utils::StartsWith(s, "publishedid")) - this->PublishedId = s.substr(12); - } - - } - if (mod != "") - { - string file = ParseString(Filesystem::ReadAllText(mod)); - vector instructions = Utils::Split(file, ";"); - for (string s : instructions) - { - auto keyValue = Utils::SplitFirst(s, "="); - auto const key = Utils::Replace(Utils::Trim(keyValue.first), "\"", ""); - auto const value = Utils::Replace(Utils::Trim(keyValue.second), "\"", ""); - if (key == "name") - this->Name = value; - else if (key == "picture") - this->Picture = value; - else if (key == "logoSmall") - this->LogoSmall = value; - else if (key == "logo") - this->Logo = value; - else if (key == "logoOver") - this->LogoOver = value; - else if (key == "action") - this->Action = value; - else if (key == "actionName") - this->ActionName = value; - else if (key == "tooltipOwned") - this->TooltipOwned = value; - else if (key == "overview") - this->Overview = value; - else if (key == "description") - this->Description = value; - else if (key == "overviewPicture") - this->OverviewPicture = value; - else if (key == "overviewText") - this->OverviewText = value; - else if (key == "author") - this->Author = value; - /* there is no stabilized syntax upon this - * some write hideName=0 or hideName=1 - * but also I've seen hideName="false" - * */ - else if (key == "hideName") - { - for (char c : value) - { - if (c == '0' || c == 'f') - { - this->HideName = false; - break; - } - else if (c == '1' || c == 't') - { - this->HideName = true; - break; - } - } - } - else if (key == "hidePicture") - { - for (char c : value) - { - if (c == '0' || c == 'f') - { - this->HidePicture = false; - break; - } - else if (c == '1' || c == 't') - { - this->HidePicture = true; - break; - } - } - } - } - } - if (workshopId == "-1") - { - this->DirName = Utils::Replace(path, Settings::ArmaPath + "/", ""); - } - if (this->DirName.empty()) - { - this->DirName = this->Name; - } - this->DirName = Utils::Replace(this->DirName, "/", "_"); -} - -string Mod::ToString() -{ - string response = ""; - - response += "Path: " + Path + "\n"; - response += "Name: " + Name + "\n"; - response += "Picture: " + Picture + "\n"; - response += "LogoSmall: " + LogoSmall + "\n"; - response += "Logo: " + Logo + "\n"; - response += "LogoOver: " + LogoOver + "\n"; - response += "Action: " + Action + "\n"; - response += "ActionName: " + ActionName + "\n"; - response += "TooltipOwned: " + TooltipOwned + "\n"; - response += "Overview: " + Overview + "\n"; - response += "Description: " + Description + "\n"; - response += "OverviewPicture: " + OverviewPicture + "\n"; - response += "OverviewText: " + OverviewText + "\n"; - response += "Author: " + Author + "\n"; - - response += "DlcColor r:" + to_string(DlcColor.r) + " g:" + to_string(DlcColor.g) - + " b:" + to_string(DlcColor.b) + " a:" + to_string(DlcColor.a) + "\n"; - - response += "HideName: " + to_string(HideName) + "\n"; - response += "HidePicture: " + to_string(HidePicture) + "\n"; - - response += "PublishedId: " + PublishedId + "\n"; - - return response; -} diff --git a/Mod.h b/Mod.h deleted file mode 100644 index 7f56cda..0000000 --- a/Mod.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Mod.h - * - * Created on: 18 Oct 2016 - * Author: muttley - */ - -#ifndef MOD_H_ -#define MOD_H_ - -#include - -#include "Vec4.h" - -class Mod -{ - public: - Mod(std::string path, std::string workshopId); - ~Mod(); - - std::string Path, Name, DirName, Picture, LogoSmall, Logo, LogoOver, Action, - ActionName, TooltipOwned, Overview, Description, OverviewPicture, - OverviewText, Author; - Vec4 DlcColor; - bool HideName, HidePicture; - std::string PublishedId; - std::string WorkshopId; - - std::string ToString(); - - bool IsRepresentedBySymlink; - - bool Enabled; - - private: - void ParseCPP(std::string meta, std::string mod, std::string path, std::string workshopId); - std::string ParseString(std::string input); -}; - -#endif /* MOD_H_ */ diff --git a/README.md b/README.md index 40f2027..b9d0bd9 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,46 @@ # ArmA 3 Unix Launcher +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/8a144e12d9cc4cde90616f0e3f282322)](https://www.codacy.com/manual/muttleyxd/arma3-unix-launcher?utm_source=github.com&utm_medium=referral&utm_content=muttleyxd/arma3-unix-launcher&utm_campaign=Badge_Grade), [![Build Status](https://cloud.drone.io/api/badges/muttleyxd/arma3-unix-launcher/status.svg)](https://cloud.drone.io/muttleyxd/arma3-unix-launcher), [![Build Status](https://travis-ci.com/muttleyxd/arma3-unix-launcher.svg?branch=master)](https://travis-ci.com/muttleyxd/arma3-unix-launcher) + ArmA 3 Launcher for Linux and Mac. Since Bohemia didn't port their launcher to Linux and Mac and existing launcher didn't satisfy my needs I decided to create my own. Launcher detects and symlinks all mods from Workshop and ArmA's main directory. It also allows to add your own mods from outside ArmA directory. -### Features - -* Read location of ArmA 3 (config.vdf parsing) -* Workshop mods support (symlink to ~arma/!workshop) -* Detect @mods (ArmA main dir) -* Add mods from outside ArmA's dir (symlink) -* Launch ArmA with desired options -* Cross platform - Linux and Mac +## Table of contents -### TODO +* [Installing](#installing) + * [From package](#from-package) + * [Building from source](#building-from-source) +* [Launch parameters](#launch-parameters) -* Steam integration (info about downloading) -* Server browser -* .paa reading - for displaying mod images in launcher -* Mod dependency caching from Steam Workshop -* MacOS binary build -* Code refactoring (logic should be outside GUI code) -* PPA for Ubuntu -### Installing - -#### Debian based (Debian, Ubuntu) +## Installing -Please build arma3-unix-launcher from source. -.deb files are outdated +### From package -~~There is a .deb package available in releases tab!~~ +For Debian based distributions (Debian, Ubuntu), Arch based distributions (Arch, Manjaro) and Mac OS X there are packages available in [releases tab!](https://github.com/muttleyxd/arma3-unix-launcher/releases) -[Releases tab](https://github.com/muttleyxd/arma3-unix-launcher/releases) +For Arch based distributions there's an AUR package available - it's called `arma3-linux-launcher-git` -#### Arch based (Arch, Antergos, Manjaro) - -For now there's AUR package available - it's called arma3-linux-launcher-git. - - yaourt -S arma3-linux-launcher-git - -#### Mac OS X - -Binary package is currently unavailable. Please build from source or use [install script](https://github.com/muttleyxd/arma3-unix-launcher/tree/mac_installer). + yay -S arma3-linux-launcher-git ### Building from source -Gtkmm3 is required +Requirements: +- GCC 8 or newer (Clang with C++17 support should work too) +- CMake 3.11 +- Qt5 with SVG support +- fmt (optional) -#### Debian package - apt-get install cmake libgtkmm-3.0-1v5 libgtkmm-3.0-dev +#### Debian based (Debian, Ubuntu) + apt install cmake qt5-default libqt5widgets5 libqt5svg5 libqt5svg5-dev libfmt-dev -#### Arch Linux - pacman -S cmake gtkmm3 +#### Arch based (Arch Linux, Manjaro) + pacman -S cmake fmt qt5-base qt5-svg #### Mac OS X - brew install gtkmm3 + brew install gcc cmake qt #### Build process git clone https://github.com/muttleyxd/arma3-unix-launcher.git @@ -68,41 +52,25 @@ Gtkmm3 is required After that you can launch with - ./arma3-unix-launcher - -Or install globally with - - sudo make install - -#### Launching on Linux + ./src/arma3-unix-launcher - arma3_unix_launcher - -#### Launching on Mac OS X - -Go to Applications and run "arma3-unix-launcher" ### Launch parameters - --verbose - -This enables verbose logging - useful if something works in a wrong way - - --purge - -This deletes all files created by the launcher (symlinks and config files) - - --preset-to-run - -This allows you to use saved mod preset and run Arma instantly, useful for desktop shortcuts - - -### Screenshots - -Mods tab: - -![mods tab](http://i.imgur.com/OmN0IDe.png) - -Parameters tab: - -![parameters tab](http://i.imgur.com/IseHvUc.png) +``` +Usage: arma3-unix-launcher [options] + +Optional arguments: +-h --help show this help message and exit +-l --list-presets list available mod presets +-p --preset-to-run preset to run, launcher will start Arma with given mods and exit +--server-ip server ip to connect to, usable only with --preset-to-run +--server-port server port to connect to, usable only with --preset-to-run +--server-password server pasword to connect to, usable only with --preset-to-run +-v --verbose verbose mode which enables more logging +``` + +Example: +``` +arma3-unix-launcher --preset-to-run testmod --server-ip 127.0.0.1 --server-port 1234 --server-password asdasd +``` diff --git a/Settings.cpp b/Settings.cpp deleted file mode 100644 index 8f8d8e4..0000000 --- a/Settings.cpp +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Settings.cpp - * - * Created on: 14 Oct 2016 - * Author: muttley - */ - -#include "Settings.h" - -#include -#include -#include -#include - -#include "Filesystem.h" -#include "Utils.h" -#include "Logger.h" - -using namespace std; - -namespace Settings -{ - string ArmaPath = Filesystem::DIR_NOT_FOUND; - string WorkshopPath = Filesystem::DIR_NOT_FOUND; - - int WindowPosX = 0; - int WindowPosY = 0; - int WindowSizeX = 800; - int WindowSizeY = 600; - - bool SkipIntro = false; - bool Nosplash = false; - bool Window = false; - bool Name = false; - string NameValue = ""; - - bool ParameterFile = false; - string ParameterFileValue = ""; - - bool CheckSignatures = false; - - bool CpuCount = false; - int CpuCountValue = 4; - - bool ExThreads = false; - bool ExThreadsFileOperations = false; - bool ExThreadsTextureLoading = false; - bool ExThreadsGeometryLoading = false; - - bool EnableHT = false; - bool DisableMulticore = false; - bool HugePages = false; - - bool FilePatching = false; - bool NoLogs = false; - bool ShowScriptErrors = false; - - bool World = false; - string WorldValue = ""; - - bool NoPause = false; - - bool Connect = false; - string ConnectValue = ""; - - bool Port = false; - string PortValue = ""; - - bool Password = false; - string PasswordValue = ""; - - bool Host = false; - - string ModPreset = "default"; - - std::vector WorkshopModsEnabled; - std::vector WorkshopModsOrder; - - std::vector CustomModsEnabled; - std::vector CustomModsOrder; - - std::vector CustomMods; - - std::string PresetToRun = ""; - - bool Load(string path) - { - string loadedFile = Filesystem::ReadAllText(path); - if (loadedFile != Filesystem::FILE_NOT_OPEN) - { - WorkshopModsEnabled.clear(); - WorkshopModsOrder.clear(); - - CustomModsEnabled.clear(); - CustomModsOrder.clear(); - - string currentPath = ""; - for (string line : Utils::Split(loadedFile, "\n")) - { - if (Utils::StartsWith(line, "ArmaPath=")) - ArmaPath = line.substr(9); - else if (Utils::StartsWith(line, "WorkshopPath=")) - WorkshopPath = line.substr(13); - else if (Utils::StartsWith(line, "WindowSizeX=")) - WindowSizeX = strtol(line.substr(12).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "WindowSizeY=")) - WindowSizeY = strtol(line.substr(12).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "WindowPosX=")) - WindowPosX = strtol(line.substr(11).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "WindowPosY=")) - WindowPosY = strtol(line.substr(11).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "SkipIntro=")) - SkipIntro = strtol(line.substr(10).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "Nosplash=")) - Nosplash = strtol(line.substr(9).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "Window=")) - Window = strtol(line.substr(7).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "Name=")) - Name = strtol(line.substr(5).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "NameValue=")) - NameValue = line.substr(10); - else if (Utils::StartsWith(line, "ParameterFile=")) - ParameterFile = strtol(line.substr(14).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "ParameterFileValue=")) - ParameterFileValue = line.substr(19); - else if (Utils::StartsWith(line, "CheckSignatures=")) - CheckSignatures = strtol(line.substr(16).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "CpuCount=")) - CpuCount = strtol(line.substr(9).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "CpuCountValue=")) - CpuCountValue = strtol(line.substr(14).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "ExThreads=")) - ExThreads = strtol(line.substr(10).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "ExThreadsFileOperations=")) - ExThreadsFileOperations = strtol(line.substr(24).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "ExThreadsTextureLoading=")) - ExThreadsTextureLoading = strtol(line.substr(24).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "ExThreadsGeometryLoading=")) - ExThreadsGeometryLoading = strtol(line.substr(25).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "EnableHT=")) - EnableHT = strtol(line.substr(9).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "DisableMulticore=")) - DisableMulticore = strtol(line.substr(17).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "HugePages=")) - HugePages = strtol(line.substr(10).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "FilePatching=")) - FilePatching = strtol(line.substr(13).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "NoLogs=")) - NoLogs = strtol(line.substr(7).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "ShowScriptErrors=")) - ShowScriptErrors = strtol(line.substr(17).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "World=")) - World = strtol(line.substr(6).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "WorldValue=")) - WorldValue = line.substr(11); - else if (Utils::StartsWith(line, "NoPause=")) - NoPause = strtol(line.substr(8).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "Connect=")) - Connect = strtol(line.substr(8).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "ConnectValue=")) - ConnectValue = line.substr(13); - else if (Utils::StartsWith(line, "Port=")) - Port = strtol(line.substr(5).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "PortValue=")) - PortValue = line.substr(10); - else if (Utils::StartsWith(line, "Password=")) - Password = strtol(line.substr(9).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "PasswordValue=")) - PasswordValue = line.substr(14); - else if (Utils::StartsWith(line, "Host=")) - Host = strtol(line.substr(5).c_str(), NULL, 10); - else if (Utils::StartsWith(line, "ModPreset=")) - ModPreset = line.substr(10); - else if (Utils::StartsWith(line, "WorkshopModsEnabled=")) - { - string sub = line.substr(20); - for (string s : Utils::Split(sub, ",")) - WorkshopModsEnabled.push_back(s); - } - else if (Utils::StartsWith(line, "WorkshopModsOrder=")) - { - string sub = line.substr(18); - for (string s : Utils::Split(sub, ",")) - WorkshopModsOrder.push_back(s); - } - else if (Utils::StartsWith(line, "CustomModsEnabled=")) - { - string sub = line.substr(18); - for (string s : Utils::Split(sub, ",")) - CustomModsEnabled.push_back(s); - } - else if (Utils::StartsWith(line, "CustomModsOrder=")) - { - string sub = line.substr(16); - for (string s : Utils::Split(sub, ",")) - CustomModsOrder.push_back(s); - } - else - LOG(1, "Invalid line in config file " + path); - } - } - else - { - LOG(1, "Can't load settings!"); - return false; - } - return true; - } - - bool Save(string path) - { - string outFile = "ArmaPath=" + ArmaPath - + "\nWorkshopPath=" + WorkshopPath - + "\nWindowSizeX=" + to_string(WindowSizeX) - + "\nWindowSizeY=" + to_string(WindowSizeY) - + "\nWindowPosX=" + to_string(WindowPosX) - + "\nWindowPosY=" + to_string(WindowPosY) - + "\nSkipIntro=" + Utils::ToString(SkipIntro) - + "\nNosplash=" + Utils::ToString(Nosplash) - + "\nWindow=" + Utils::ToString(Window) - + "\nName=" + Utils::ToString(Name) - + "\nNameValue=" + NameValue - + "\nParameterFile=" + Utils::ToString(ParameterFile) - + "\nParameterFileValue=" + ParameterFileValue - + "\nCheckSignatures=" + Utils::ToString(CheckSignatures) - + "\nCpuCount=" + Utils::ToString(CpuCount) - + "\nCpuCountValue=" + to_string(CpuCountValue) - + "\nExThreads=" + Utils::ToString(ExThreads) - + "\nExThreadsFileOperations=" + Utils::ToString(ExThreadsFileOperations) - + "\nExThreadsTextureLoading=" + Utils::ToString(ExThreadsTextureLoading) - + "\nExThreadsGeometryLoading=" + Utils::ToString(ExThreadsGeometryLoading) - + "\nEnableHT=" + Utils::ToString(EnableHT) - + "\nDisableMulticore=" + Utils::ToString(DisableMulticore) - + "\nHugePages=" + Utils::ToString(HugePages) - + "\nFilePatching=" + Utils::ToString(FilePatching) - + "\nNoLogs=" + Utils::ToString(NoLogs) - + "\nShowScriptErrors=" + Utils::ToString(ShowScriptErrors) - + "\nWorld=" + Utils::ToString(World) - + "\nWorldValue=" + WorldValue - + "\nNoPause=" + Utils::ToString(NoPause) - + "\nConnect=" + Utils::ToString(Connect) - + "\nConnectValue=" + ConnectValue - + "\nPort=" + Utils::ToString(Port) - + "\nPortValue=" + PortValue - + "\nPassword=" + Utils::ToString(Password) - + "\nPasswordValue=" + PasswordValue - + "\nHost=" + Utils::ToString(Host) - + "\nModPreset=" + (ModPreset[ ModPreset.length() - 1 ] == '*' ? "default" : ModPreset) - + "\nWorkshopModsEnabled="; - - - for (string i : WorkshopModsEnabled) - outFile += i + ","; - - outFile += "\nWorkshopModsOrder="; - - for (string i : WorkshopModsOrder) - outFile += i + ","; - - outFile += "\nCustomModsEnabled="; - - for (string i : CustomModsEnabled) - outFile += Utils::Replace(i, Filesystem::ArmaDirMark, Settings::ArmaPath) + ","; - - outFile += "\nCustomModsOrder="; - - for (string i : CustomModsOrder) - outFile += Utils::Replace(i, Filesystem::ArmaDirMark, Settings::ArmaPath) + ","; - - if (!Filesystem::WriteAllText(path, outFile)) - { - LOG(1, "Can't write to settings file!"); - return false; - } - return true; - } - - bool ModEnabled(string workshopId) - { - if (workshopId.length() == 0) - return false; - else if (isdigit(workshopId[0])) - { - for (string s : WorkshopModsEnabled) - { - if (s == workshopId) - return true; - } - } - else - { - for (string s : CustomModsEnabled) - { - if (s == workshopId) - return true; - } - } - return false; - } -} diff --git a/Settings.h b/Settings.h deleted file mode 100644 index 50e8ed7..0000000 --- a/Settings.h +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Settings.h - * - * Created on: 14 Oct 2016 - * Author: muttley - */ - -#ifndef SETTINGS_H_ -#define SETTINGS_H_ - -#include -#include - -#include "Mod.h" - -namespace Settings -{ - extern std::string ArmaPath; - extern std::string WorkshopPath; - - extern int WindowSizeX; - extern int WindowSizeY; - extern int WindowPosX; - extern int WindowPosY; - - extern bool SkipIntro; - extern bool Nosplash; - extern bool Window; - extern bool Name; - extern std::string NameValue; - - extern bool ParameterFile; - extern std::string ParameterFileValue; - - extern bool CheckSignatures; - - extern bool CpuCount; - extern int CpuCountValue; - - extern bool ExThreads; - extern bool ExThreadsFileOperations; - extern bool ExThreadsTextureLoading; - extern bool ExThreadsGeometryLoading; - - extern bool EnableHT; - extern bool DisableMulticore; - extern bool HugePages; - - extern bool FilePatching; - extern bool NoLogs; - extern bool ShowScriptErrors; - - extern bool World; - extern std::string WorldValue; - - extern bool NoPause; - - extern bool Connect; - extern std::string ConnectValue; - - extern bool Port; - extern std::string PortValue; - - extern bool Password; - extern std::string PasswordValue; - - extern bool Host; - - extern std::string ModPreset; - - extern std::vector WorkshopModsEnabled; - extern std::vector WorkshopModsOrder; - - extern std::vector CustomModsEnabled; - extern std::vector CustomModsOrder; - - bool Load(std::string path); - bool Save(std::string path); - - bool ModEnabled(std::string workshopId); - - extern std::string PresetToRun; -}; - -#endif /* SETTINGS_H_ */ diff --git a/Utils.cpp b/Utils.cpp deleted file mode 100644 index 9cb9cc8..0000000 --- a/Utils.cpp +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Utils.cpp - * - * Created on: 20 Oct 2016 - * Author: muttley - */ - -#include "Utils.h" - -#include -#include - -#include -#include -#include - -#include "Filesystem.h" -#include "Logger.h" - -using namespace std; - -namespace Utils -{ - vector Split(std::string textToSplit, std::string delimiters) - { - vector response; - char *text = new char[textToSplit.size() + 1]; - strcpy(text, textToSplit.c_str()); - char *strPtr = strtok(text, delimiters.c_str()); - while (strPtr != NULL) - { - string toAdd = strPtr; - toAdd = Trim(toAdd); - if (toAdd.length() > 0) - response.push_back(strPtr); - strPtr = strtok(NULL, delimiters.c_str()); - } - delete[] text; - return response; - } - - pair SplitFirst(string textToSplit, string delimiters) - { - auto position = textToSplit.find_first_of(delimiters); - return pair {textToSplit.substr(0, position), textToSplit.substr(position + 1)}; - } - - bool EndsWith(string textToCheck, string textToFind) - { - if (textToFind.size() > textToCheck.size()) - return false; - char *textToCheckPtr = (char *)textToCheck.c_str() + textToCheck.size() - textToFind.size(); - if (strcmp(textToCheckPtr, textToFind.c_str()) == 0) - return true; - return false; - } - - bool StartsWith(string textToCheck, string textToFind) - { - if (textToFind.size() > textToCheck.size()) - return false; - if (strncmp(textToCheck.c_str(), textToFind.c_str(), textToFind.size()) == 0) - return true; - return false; - } - - string Replace(string str, string from, string to) - { - size_t start_pos = 0; - while (start_pos != std::string::npos) - { - start_pos = str.find(from, start_pos); - if (start_pos == std::string::npos) - return str; - str.replace(start_pos, from.length(), to); - //move start_pos forward so it doesn't replace the same string over and over - start_pos += to.length() - from.length() + 1; - } - return str; - } - - string Trim(const string &s) - { - return TrimRight(TrimLeft(s)); - } - - string TrimLeft(const string &s) - { - size_t startpos = s.find_first_not_of(" \n\r\t"); - return (startpos == std::string::npos) ? "" : s.substr(startpos); - } - - string TrimRight(const string &s) - { - size_t endpos = s.find_last_not_of(" \n\r\t"); - return (endpos == std::string::npos) ? "" : s.substr(0, endpos + 1); - } - - string RemoveLastElement(string s, bool removeSlash, int count) - { - if (s.length() == 0) - return ""; - reverse(s.begin(), s.end()); - - for (int i = 0; i < count; i++) - { - size_t slashPos = s.find("/"); - slashPos++; - - s = s.substr(slashPos); - } - if (!removeSlash) - s = "/" + s; - reverse(s.begin(), s.end()); - - return s; - } - - string ToString(bool b) - { - if (b) - return "1"; - return "0"; - } - - pid_t FindProcess(string name) - { - #ifdef __APPLE__ - char buffer[128]; - std::string result = ""; - std::string cmd = "ps -Ac | grep " + name; - std::shared_ptr pipe(popen(cmd.c_str(), "r"), pclose); - if (!pipe) throw std::runtime_error("popen() failed!"); - while (!feof(pipe.get())) - { - if (fgets(buffer, 128, pipe.get()) != NULL) - result += buffer; - } - if (result.length() < 1) - return -1; - vector splits = Split(result, " "); - if (splits.size() > 0) - return strtol(splits[0].c_str(), NULL, 10); - return -1; - #else - - for (auto const &process : Filesystem::GetSubDirectories("/proc")) - { - auto process_name = Utils::Trim(Filesystem::ReadAllText("/proc/" + process + "/comm", true)); - if (process_name == name) - return std::stoi(process); - } - - return -1; - #endif - } - - string BashAdaptPath(string path) - { - return Utils::Replace(path, " ", "\\ "); - } - - bool ContainsAddons(const std::string &path) - { - for (auto dir : Filesystem::GetSubDirectories(path)) - { - std::transform(dir.begin(), dir.end(), dir.begin(), ::tolower); - if (dir == "addons") - return true; - } - return false; - } - - bool ContainsCppFile(const std::string &path) - { - return Filesystem::FileExists(path + "/meta.cpp") || Filesystem::FileExists(path + "/mod.cpp"); - } -} diff --git a/Utils.h b/Utils.h deleted file mode 100644 index e5d53ce..0000000 --- a/Utils.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Utils.h - * - * Created on: 20 Oct 2016 - * Author: muttley - */ - -#ifndef UTILS_H_ -#define UTILS_H_ - -#include -#include -#include - -#include -#include - -namespace Utils -{ - std::vector Split(std::string textToSplit, std::string delimiters); - std::pair SplitFirst(std::string textToSplit, std::string delimiters); - bool EndsWith(std::string textToCheck, std::string textToFind); - bool StartsWith(std::string textToCheck, std::string textToFind); - std::string Replace(std::string str, std::string from, std::string to); - std::string Trim(const std::string &s); - std::string TrimLeft(const std::string &s); - std::string TrimRight(const std::string &s); - std::string RemoveLastElement(std::string s, bool removeSlash, int count = 1); - std::string ToString(bool b); - pid_t FindProcess(std::string name); - std::string BashAdaptPath(std::string path); - bool ContainsAddons(const std::string &path); - bool ContainsCppFile(const std::string &path); -}; - -#endif /* UTILS_H_ */ diff --git a/VDF.cpp b/VDF.cpp deleted file mode 100644 index d119d70..0000000 --- a/VDF.cpp +++ /dev/null @@ -1,53 +0,0 @@ -/* - * VDF.cpp - * - * Created on: 9 Oct 2016 - * Author: muttley - */ - -#include "VDF.h" - -#include -#include - -#include "Utils.h" - -using namespace std; - -VDF::VDF(string text) -{ - istringstream iss(text); - string currentPath = ""; - int counter = 0; - for (string line; getline(iss, line); counter++) - { - vector splits = Utils::Split(Utils::Trim(line), "\""); - if (splits.size() == 0) - continue; - string newPath; - switch (splits[0][0]) - { - case '{': - currentPath += '/'; - break; - case '}': - currentPath = Utils::RemoveLastElement(currentPath, true); - break; - default: - currentPath = Utils::RemoveLastElement(currentPath, false) + splits[0]; - if (splits.size() > 1) - Keys.push_back(VDFKey(currentPath, splits[1])); - else - Keys.push_back(VDFKey(currentPath, "")); - break; - } - } -} - -string VDF::GetValue(string KeyName) -{ - for (VDFKey v : Keys) - if (v.Path == KeyName) - return v.Value; - return KEY_NOT_FOUND; -} diff --git a/VDF.h b/VDF.h deleted file mode 100644 index b717afa..0000000 --- a/VDF.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * VDF.h - * - * Created on: 9 Oct 2016 - * Author: muttley - */ - -#ifndef VDF_H_ -#define VDF_H_ - -#include "VDFKey.h" -#include - -#define KEY_NOT_FOUND "KEY_WAS_NOT_FOUND_IN_VDF_FILE" - -class VDF -{ - public: - VDF(std::string filename); - std::vector Keys; - std::string GetValue(std::string KeyName); -}; - -#endif /* VDF_H_ */ diff --git a/VDFKey.cpp b/VDFKey.cpp deleted file mode 100644 index 55a987a..0000000 --- a/VDFKey.cpp +++ /dev/null @@ -1,15 +0,0 @@ -/* - * VDFKey.cpp - * - * Created on: 9 Oct 2016 - * Author: muttley - */ - -#include "VDFKey.h" - -VDFKey::VDFKey(std::string path, std::string value) -{ - Path = path; - Value = value; -} - diff --git a/VDFKey.h b/VDFKey.h deleted file mode 100644 index e126673..0000000 --- a/VDFKey.h +++ /dev/null @@ -1,21 +0,0 @@ -/* - * VDFKey.h - * - * Created on: 9 Oct 2016 - * Author: muttley - */ - -#ifndef VDFKEY_H_ -#define VDFKEY_H_ - -#include - -class VDFKey -{ - public: - VDFKey(std::string path, std::string value); - - std::string Path, Value; -}; - -#endif /* VDFKEY_H_ */ diff --git a/Vec4.h b/Vec4.h deleted file mode 100644 index 9aeefd1..0000000 --- a/Vec4.h +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Vec4.h - * - * Created on: 19 Oct 2016 - * Author: muttley - */ - -#ifndef VEC4_H_ -#define VEC4_H_ - -struct Vec4 -{ - float r, g, b, a; -}; - - - -#endif /* VEC4_H_ */ diff --git a/arma3-unix-launcher.desktop b/arma3-unix-launcher.desktop deleted file mode 100644 index f629e9b..0000000 --- a/arma3-unix-launcher.desktop +++ /dev/null @@ -1,7 +0,0 @@ -[Desktop Entry] -Type=Application -Name=ArmA 3 Unix Launcher -Comment=Advanced launcher for ArmA 3 on Linux and Mac -Exec=arma3-unix-launcher -Categories=Game; -StartupNotify=true diff --git a/astyle-format.sh b/astyle-format.sh index 9bc75b1..28144bd 100755 --- a/astyle-format.sh +++ b/astyle-format.sh @@ -1,2 +1,14 @@ #!/bin/bash -astyle --options=astylerc -n *.cpp *.h +pushd `dirname "$(readlink -f "$0")"` + +dirs=( "include" "include/exceptions" "src/arma3-unix-launcher" "src/arma3-unix-launcher-library" "src/arma3-unix-launcher-library/exceptions" "tests/include" "tests/src" ) +astylerc=`realpath astylerc` +for directory in "${dirs[@]}" +do + pushd $directory + astyle --options=$astylerc -n *.cpp + astyle --options=$astylerc -n *.hpp + popd +done + +popd diff --git a/astylerc b/astylerc index 921aee3..6984e5f 100644 --- a/astylerc +++ b/astylerc @@ -16,3 +16,4 @@ pad-header align-pointer=name align-reference=name convert-tabs +max-code-length=120 diff --git a/build-deb.sh b/build-deb.sh deleted file mode 100755 index 988c9a8..0000000 --- a/build-deb.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -cat /etc/*-release | grep debian > /dev/null -if [ $? -ne 0 ]; then - echo "You are not Debian-based distro!" - exit 1 -fi - -shortCommitHash=`git rev-parse --verify HEAD | cut -c -7` -branchName=`git branch | grep "*" | cut -c 3-` -commitCount=`git rev-list HEAD --count` -distroName=`cat /etc/*-release | grep "^ID=.*" | cut -c 4-` -architecture=`arch` - -if [ $architecture == "x86_64" ]; then - debConfigArchitecture="amd64"; -else - debConfigArchitecture="i386"; -fi - -dirName="arma3-unix-launcher_$branchName-$commitCount-$shortCommitHash-$distroName-$architecture" - -rm -rf build-deb-pkg/ -mkdir build-deb-pkg/ -cd build-deb-pkg - -cmake .. -make -j -make install DESTDIR="$(pwd)/$dirName" - -cd $dirName -mkdir DEBIAN - -echo "Package: arma3-unix-launcher" > DEBIAN/control -echo "Version: $commitCount-$shortCommitHash" >> DEBIAN/control -echo "Section: base" >> DEBIAN/control -echo "Priority: optional" >> DEBIAN/control -echo "Architecture: $debConfigArchitecture" >> DEBIAN/control -echo "Depends: libgtkmm-3.0-dev (>= 3.14.0)" >> DEBIAN/control -echo "Maintainer: Muttley " >> DEBIAN/control -echo "Description: Advanced launcher Linux and Mac ArmA 3" >> DEBIAN/control - -cd .. -dpkg-deb --build $dirName diff --git a/cmake/external_dependencies.cmake b/cmake/external_dependencies.cmake new file mode 100644 index 0000000..139312a --- /dev/null +++ b/cmake/external_dependencies.cmake @@ -0,0 +1,112 @@ +include(CheckCXXSourceCompiles) +include(FetchContent) + +function(setup_library SOURCE_TO_TEST) + set(boolArgs HEADER_ONLY) + set(oneValueArgs NAME GIT_REPOSITORY TEST_LINK_LIBS) + set(multiValueArgs WHEN) + cmake_parse_arguments(LIB_SETUP "${boolArgs}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + set(TEST_NAME "SYSTEM_${LIB_SETUP_NAME}_WORKS") + set(CMAKE_REQUIRED_LIBRARIES "${LIB_SETUP_TEST_LINK_LIBS}") + check_cxx_source_compiles("${SOURCE_TO_TEST}" ${TEST_NAME}) + + if (${TEST_NAME}) + if (LIB_SETUP_HEADER_ONLY) + add_library(${LIB_SETUP_NAME} INTERFACE) + else() + find_package(${LIB_SETUP_NAME} REQUIRED) + endif() + return() + endif() + + FetchContent_Declare(${LIB_SETUP_NAME} + GIT_REPOSITORY ${LIB_SETUP_GIT_REPOSITORY}) + FetchContent_GetProperties(${LIB_SETUP_NAME}) + set(POPULATED "${LIB_SETUP_NAME}_POPULATED") + if (NOT "${POPULATED}") + message("-- Downloading ${LIB_SETUP_NAME} from ${LIB_SETUP_GIT_REPOSITORY}") + FetchContent_Populate(${LIB_SETUP_NAME}) + set(SRCDIR "${LIB_SETUP_NAME}_SOURCE_DIR") + set(BINDIR "${LIB_SETUP_NAME}_BINARY_DIR") + + add_subdirectory(${${SRCDIR}} ${${BINDIR}} EXCLUDE_FROM_ALL) + message("-- Using external ${LIB_SETUP_NAME}") + endif() +endfunction() + +function(setup_argparse) + set(CHECK_SOURCE "#include + int main() + { + return 0; + }") + setup_library("${CHECK_SOURCE}" + NAME argparse + GIT_REPOSITORY https://github.com/p-ranav/argparse.git + HEADER_ONLY + ) + if (NOT TARGET argparse::argparse) + add_library(argparse::argparse ALIAS argparse) + endif() +endfunction() + +function(setup_doctest) + set(CHECK_SOURCE "#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + #include ") + setup_library("${CHECK_SOURCE}" + NAME doctest + GIT_REPOSITORY https://github.com/onqtam/doctest.git + HEADER_ONLY + ) + add_library(doctest::doctest ALIAS doctest) +endfunction() + +function(setup_fmt) + set(CHECK_SOURCE "#include + int main() + { + fmt::print(\"hello\"); + return 0; + }") + setup_library("${CHECK_SOURCE}" + NAME fmt + GIT_REPOSITORY https://github.com/fmtlib/fmt.git + TEST_LINK_LIBS fmt + ) +endfunction() + +function(setup_nlohmann_json) + set(CHECK_SOURCE "#include + int main() + { + nlohmann::json json; + json[0] = 1; + return 0; + }") + check_cxx_source_compiles("${CHECK_SOURCE}" SYSTEM_nlohmann_json_WORKS) + if (SYSTEM_nlohmann_json_WORKS) + add_library(nlohmann_json INTERFACE) + else() + FetchContent_Declare(nlohmann_json + URL https://github.com/nlohmann/json/releases/download/v3.7.3/include.zip) + FetchContent_GetProperties(nlohmann_json) + if (NOT nlohmann_json_POPULATED) + FetchContent_Populate(nlohmann_json) + add_library(nlohmann_json INTERFACE) + target_include_directories(nlohmann_json INTERFACE ${nlohmann_json_SOURCE_DIR}/include) + endif() + endif() + add_library(nlohmann::json ALIAS nlohmann_json) +endfunction() + +function(setup_trompeloeil) + set(CHECK_SOURCE "#include + #include ") + setup_library("${CHECK_SOURCE}" + NAME trompeloeil + GIT_REPOSITORY https://github.com/rollbear/trompeloeil.git + HEADER_ONLY + ) + add_library(trompeloeil::trompeloeil ALIAS trompeloeil) +endfunction() diff --git a/cmake/functions.cmake b/cmake/functions.cmake new file mode 100644 index 0000000..d86ad2e --- /dev/null +++ b/cmake/functions.cmake @@ -0,0 +1,94 @@ +function(create_test) + set(oneValueArgs TEST_GROUP TEST_NAME) + set(multiValueArgs INCLUDES LINK_LIBS SOURCES) + cmake_parse_arguments(CREATE_TEST "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + set(NAME "${CREATE_TEST_TEST_NAME}") + set(GROUP "${CREATE_TEST_TEST_GROUP}") + set(INCLUDES "${CREATE_TEST_INCLUDES}") + set(LINK_LIBS "${CREATE_TEST_LINK_LIBS}") + + add_executable(${GROUP}_${NAME} ${CREATE_TEST_SOURCES}) + target_include_directories(${GROUP}_${NAME} PRIVATE ${INCLUDES} ${CMAKE_SOURCE_DIR}/tests) + target_link_libraries(${GROUP}_${NAME} PRIVATE ${LINK_LIBS} doctest::doctest trompeloeil::trompeloeil) + add_test(${GROUP}_${NAME} ${GROUP}_${NAME}) +endfunction() + +function(setup_objects) + set(oneValueArgs TARGET_NAME OUTPUT_TARGET_LIST) + set(multiValueArgs INCLUDES SOURCES) + cmake_parse_arguments(SETUP_OBJECTS "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + set(OUTPUT_TARGETS "") + + foreach(SOURCE_FILE ${SETUP_OBJECTS_SOURCES}) # src/common/some_file.cpp + get_filename_component(SOURCE_NAME_DIRECTORY "${SOURCE_FILE}" DIRECTORY) # src/common or empty + string(REPLACE "/" "_" SOURCE_NAME_DIRECTORY_TARGET_NAME "${SOURCE_NAME_DIRECTORY}") # src_common or empty + + set(TARGET_NAME_PREFIX "obj_${SETUP_OBJECTS_TARGET_NAME}") # obj_module + if (NOT "${SOURCE_NAME_DIRECTORY_TARGET_NAME}" STREQUAL "") + set(TARGET_NAME_PREFIX "${TARGET_NAME_PREFIX}_${SOURCE_NAME_DIRECTORY_TARGET_NAME}") # obj_module_src_common + endif() + + get_filename_component(SOURCE_NAME_WE "${SOURCE_FILE}" NAME_WE) # some_file + set(TARGET_NAME "${TARGET_NAME_PREFIX}_${SOURCE_NAME_WE}") # obj_module_generated_api_some_file + + get_filename_component(SOURCE_EXTENSION "${SOURCE_FILE}" EXT) # .cpp or .hpp + if (TARGET ${TARGET_NAME} AND "${SOURCE_EXTENSION}" STREQUAL ".hpp") + target_sources(${TARGET_NAME} PRIVATE ${SOURCE_FILE}) # add header to target + endif() + + if ("${SOURCE_EXTENSION}" STREQUAL ".cpp") + add_library(${TARGET_NAME} OBJECT ${SOURCE_FILE}) + target_compile_options(${TARGET_NAME} PRIVATE "-fpic") + target_include_directories(${TARGET_NAME} PUBLIC ${SETUP_OBJECTS_INCLUDES}) + list(APPEND OUTPUT_TARGETS $) + endif() + endforeach() + + if (NOT "${SETUP_OBJECTS_OUTPUT_TARGET_LIST}" STREQUAL "") + set("${SETUP_OBJECTS_OUTPUT_TARGET_LIST}" "${OUTPUT_TARGETS}" PARENT_SCOPE) + endif() +endfunction() + +function(add_flags_to_compiler) + set(oneValueArgs COMPILER_ID FLAGS WARNING_MESSAGE) + set(multiValueArgs WARN_WHEN WHEN) + cmake_parse_arguments(ADD_FLAGS "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if (NOT "${ADD_FLAGS_WHEN}" STREQUAL "") + if (NOT ${ADD_FLAGS_WHEN}) + return() + endif() + endif() + + if (NOT "${ADD_FLAGS_WARN_WHEN}" STREQUAL "") + if (${ADD_FLAGS_WARN_WHEN}) + message(WARNING "Warning: ${ADD_FLAGS_WARNING_MESSAGE}; ${ADD_FLAGS_WARN_WHEN}") + endif() + endif() + + if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "${ADD_FLAGS_COMPILER_ID}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${ADD_FLAGS_FLAGS}" PARENT_SCOPE) + endif() +endfunction() + +function(add_libraries_to_linker) + set(oneValueArgs COMPILER_ID LIBS WARNING_MESSAGE) + set(multiValueArgs WHEN) + cmake_parse_arguments(ADD_LIBRARIES "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if (NOT "${ADD_LIBRARIES_WHEN}" STREQUAL "") + if (NOT ${ADD_LIBRARIES_WHEN}) + return() + endif() + endif() + + if (NOT "${ADD_LIBRARIES_WARNING_MESSAGE}" STREQUAL "") + message(WARNING "Warning: ${ADD_LIBRARIES_WARNING_MESSAGE}") + endif() + + if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "${ADD_LIBRARIES_COMPILER_ID}") + link_libraries(${ADD_LIBRARIES_LIBS}) + endif() +endfunction() diff --git a/external/backward-cpp/BackwardConfig.cmake b/external/backward-cpp/BackwardConfig.cmake new file mode 100644 index 0000000..6cff86c --- /dev/null +++ b/external/backward-cpp/BackwardConfig.cmake @@ -0,0 +1,202 @@ +# +# BackwardMacros.cmake +# Copyright 2013 Google Inc. All Rights Reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +############################################################################### +# OPTIONS +############################################################################### + +set(STACK_WALKING_UNWIND TRUE CACHE BOOL + "Use compiler's unwind API") +set(STACK_WALKING_BACKTRACE FALSE CACHE BOOL + "Use backtrace from (e)glibc for stack walking") + +set(STACK_DETAILS_AUTO_DETECT TRUE CACHE BOOL + "Auto detect backward's stack details dependencies") + +set(STACK_DETAILS_BACKTRACE_SYMBOL FALSE CACHE BOOL + "Use backtrace from (e)glibc for symbols resolution") +set(STACK_DETAILS_DW FALSE CACHE BOOL + "Use libdw to read debug info") +set(STACK_DETAILS_BFD FALSE CACHE BOOL + "Use libbfd to read debug info") +set(STACK_DETAILS_DWARF FALSE CACHE BOOL + "Use libdwarf/libelf to read debug info") + +set(BACKWARD_TESTS FALSE CACHE BOOL "Enable tests") + +############################################################################### +# CONFIGS +############################################################################### +if (${STACK_DETAILS_AUTO_DETECT}) + include(FindPackageHandleStandardArgs) + + # find libdw + find_path(LIBDW_INCLUDE_DIR NAMES "elfutils/libdw.h" "elfutils/libdwfl.h") + find_library(LIBDW_LIBRARY dw) + set(LIBDW_INCLUDE_DIRS ${LIBDW_INCLUDE_DIR} ) + set(LIBDW_LIBRARIES ${LIBDW_LIBRARY} ) + find_package_handle_standard_args(libdw DEFAULT_MSG + LIBDW_LIBRARY LIBDW_INCLUDE_DIR) + mark_as_advanced(LIBDW_INCLUDE_DIR LIBDW_LIBRARY) + + # find libbfd + find_path(LIBBFD_INCLUDE_DIR NAMES "bfd.h") + find_path(LIBDL_INCLUDE_DIR NAMES "dlfcn.h") + find_library(LIBBFD_LIBRARY bfd) + find_library(LIBDL_LIBRARY dl) + set(LIBBFD_INCLUDE_DIRS ${LIBBFD_INCLUDE_DIR} ${LIBDL_INCLUDE_DIR}) + set(LIBBFD_LIBRARIES ${LIBBFD_LIBRARY} ${LIBDL_LIBRARY}) + find_package_handle_standard_args(libbfd DEFAULT_MSG + LIBBFD_LIBRARY LIBBFD_INCLUDE_DIR + LIBDL_LIBRARY LIBDL_INCLUDE_DIR) + mark_as_advanced(LIBBFD_INCLUDE_DIR LIBBFD_LIBRARY + LIBDL_INCLUDE_DIR LIBDL_LIBRARY) + + # find libdwarf + find_path(LIBDWARF_INCLUDE_DIR NAMES "libdwarf.h" PATH_SUFFIXES libdwarf) + find_path(LIBELF_INCLUDE_DIR NAMES "libelf.h") + find_path(LIBDL_INCLUDE_DIR NAMES "dlfcn.h") + find_library(LIBDWARF_LIBRARY dwarf) + find_library(LIBELF_LIBRARY elf) + find_library(LIBDL_LIBRARY dl) + set(LIBDWARF_INCLUDE_DIRS ${LIBDWARF_INCLUDE_DIR} ${LIBELF_INCLUDE_DIR} ${LIBDL_INCLUDE_DIR}) + set(LIBDWARF_LIBRARIES ${LIBDWARF_LIBRARY} ${LIBELF_LIBRARY} ${LIBDL_LIBRARY}) + find_package_handle_standard_args(libdwarf DEFAULT_MSG + LIBDWARF_LIBRARY LIBDWARF_INCLUDE_DIR + LIBELF_LIBRARY LIBELF_INCLUDE_DIR + LIBDL_LIBRARY LIBDL_INCLUDE_DIR) + mark_as_advanced(LIBDWARF_INCLUDE_DIR LIBDWARF_LIBRARY + LIBELF_INCLUDE_DIR LIBELF_LIBRARY + LIBDL_INCLUDE_DIR LIBDL_LIBRARY) + + if (LIBDW_FOUND) + LIST(APPEND _BACKWARD_INCLUDE_DIRS ${LIBDW_INCLUDE_DIRS}) + LIST(APPEND _BACKWARD_LIBRARIES ${LIBDW_LIBRARIES}) + set(STACK_DETAILS_DW TRUE) + set(STACK_DETAILS_BFD FALSE) + set(STACK_DETAILS_DWARF FALSE) + set(STACK_DETAILS_BACKTRACE_SYMBOL FALSE) + elseif(LIBBFD_FOUND) + LIST(APPEND _BACKWARD_INCLUDE_DIRS ${LIBBFD_INCLUDE_DIRS}) + LIST(APPEND _BACKWARD_LIBRARIES ${LIBBFD_LIBRARIES}) + + # If we attempt to link against static bfd, make sure to link its dependencies, too + get_filename_component(bfd_lib_ext "${LIBBFD_LIBRARY}" EXT) + if (bfd_lib_ext STREQUAL "${CMAKE_STATIC_LIBRARY_SUFFIX}") + list(APPEND _BACKWARD_LIBRARIES iberty z) + endif() + + set(STACK_DETAILS_DW FALSE) + set(STACK_DETAILS_BFD TRUE) + set(STACK_DETAILS_DWARF FALSE) + set(STACK_DETAILS_BACKTRACE_SYMBOL FALSE) + elseif(LIBDWARF_FOUND) + LIST(APPEND _BACKWARD_INCLUDE_DIRS ${LIBDWARF_INCLUDE_DIRS}) + LIST(APPEND BACKWARD_LIBRARIES ${LIBDWARF_LIBRARIES}) + + set(STACK_DETAILS_DW FALSE) + set(STACK_DETAILS_BFD FALSE) + set(STACK_DETAILS_DWARF TRUE) + set(STACK_DETAILS_BACKTRACE_SYMBOL FALSE) + else() + set(STACK_DETAILS_DW FALSE) + set(STACK_DETAILS_BFD FALSE) + set(STACK_DETAILS_DWARF FALSE) + set(STACK_DETAILS_BACKTRACE_SYMBOL TRUE) + endif() +else() + if (STACK_DETAILS_DW) + LIST(APPEND _BACKWARD_LIBRARIES dw) + endif() + + if (STACK_DETAILS_BFD) + LIST(APPEND _BACKWARD_LIBRARIES bfd dl) + endif() + + if (STACK_DETAILS_DWARF) + LIST(APPEND _BACKWARD_LIBRARIES dwarf elf) + endif() +endif() + +macro(map_definitions var_prefix define_prefix) + foreach(def ${ARGN}) + if (${${var_prefix}${def}}) + LIST(APPEND _BACKWARD_DEFINITIONS "${define_prefix}${def}=1") + else() + LIST(APPEND _BACKWARD_DEFINITIONS "${define_prefix}${def}=0") + endif() + endforeach() +endmacro() + +if (NOT _BACKWARD_DEFINITIONS) + map_definitions("STACK_WALKING_" "BACKWARD_HAS_" UNWIND BACKTRACE) + map_definitions("STACK_DETAILS_" "BACKWARD_HAS_" BACKTRACE_SYMBOL DW BFD DWARF) +endif() + +set(BACKWARD_INCLUDE_DIR "${CMAKE_CURRENT_LIST_DIR}") + +set(BACKWARD_HAS_EXTERNAL_LIBRARIES FALSE) +set(FIND_PACKAGE_REQUIRED_VARS BACKWARD_INCLUDE_DIR) +if(DEFINED _BACKWARD_LIBRARIES) + set(BACKWARD_HAS_EXTERNAL_LIBRARIES TRUE) + list(APPEND FIND_PACKAGE_REQUIRED_VARS _BACKWARD_LIBRARIES) +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Backward + REQUIRED_VARS ${FIND_PACKAGE_REQUIRED_VARS} +) +list(APPEND _BACKWARD_INCLUDE_DIRS ${BACKWARD_INCLUDE_DIR}) + +macro(add_backward target) + target_include_directories(${target} PRIVATE ${BACKWARD_INCLUDE_DIRS}) + set_property(TARGET ${target} APPEND PROPERTY COMPILE_DEFINITIONS ${BACKWARD_DEFINITIONS}) + set_property(TARGET ${target} APPEND PROPERTY LINK_LIBRARIES ${BACKWARD_LIBRARIES}) +endmacro() + +set(BACKWARD_INCLUDE_DIRS ${_BACKWARD_INCLUDE_DIRS} CACHE INTERNAL "_BACKWARD_INCLUDE_DIRS") +set(BACKWARD_DEFINITIONS ${_BACKWARD_DEFINITIONS} CACHE INTERNAL "BACKWARD_DEFINITIONS") +set(BACKWARD_LIBRARIES ${_BACKWARD_LIBRARIES} CACHE INTERNAL "BACKWARD_LIBRARIES") +mark_as_advanced(BACKWARD_INCLUDE_DIRS BACKWARD_DEFINITIONS BACKWARD_LIBRARIES) + +# Expand each definition in BACKWARD_DEFINITIONS to its own cmake var and export +# to outer scope +foreach(var ${BACKWARD_DEFINITIONS}) + string(REPLACE "=" ";" var_as_list ${var}) + list(GET var_as_list 0 var_name) + list(GET var_as_list 1 var_value) + set(${var_name} ${var_value}) + mark_as_advanced(${var_name}) +endforeach() + +if (NOT TARGET Backward::Backward) + add_library(Backward::Backward INTERFACE IMPORTED) + set_target_properties(Backward::Backward PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${BACKWARD_INCLUDE_DIRS}" + INTERFACE_COMPILE_DEFINITIONS "${BACKWARD_DEFINITIONS}" + ) + if(BACKWARD_HAS_EXTERNAL_LIBRARIES) + set_target_properties(Backward::Backward PROPERTIES + INTERFACE_LINK_LIBRARIES "${BACKWARD_LIBRARIES}" + ) + endif() +endif() diff --git a/external/backward-cpp/CMakeLists.txt b/external/backward-cpp/CMakeLists.txt new file mode 100644 index 0000000..7ebda29 --- /dev/null +++ b/external/backward-cpp/CMakeLists.txt @@ -0,0 +1,139 @@ +# +# CMakeLists.txt +# Copyright 2013 Google Inc. All Rights Reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +cmake_minimum_required(VERSION 2.8.12) +project(backward CXX) + +# Introduce variables: +# * CMAKE_INSTALL_LIBDIR +# * CMAKE_INSTALL_BINDIR +# * CMAKE_INSTALL_INCLUDEDIR +include(GNUInstallDirs) + +include(BackwardConfig.cmake) + +# check if compiler is nvcc or nvcc_wrapper +set(COMPILER_IS_NVCC false) +get_filename_component(COMPILER_NAME ${CMAKE_CXX_COMPILER} NAME) +if (COMPILER_NAME MATCHES "^nvcc") + set(COMPILER_IS_NVCC true) +endif() + +if (DEFINED ENV{OMPI_CXX} OR DEFINED ENV{MPICH_CXX}) + if ( ($ENV{OMPI_CXX} MATCHES "nvcc") OR ($ENV{MPICH_CXX} MATCHES "nvcc") ) + set(COMPILER_IS_NVCC true) + endif() +endif() + +# set CXX standard +set(CMAKE_CXX_STANDARD_REQUIRED True) +set(CMAKE_CXX_STANDARD 11) +if (${COMPILER_IS_NVCC}) + # GNU CXX extensions are not supported by nvcc + set(CMAKE_CXX_EXTENSIONS OFF) +endif() + +############################################################################### +# COMPILER FLAGS +############################################################################### + +if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_COMPILER_IS_GNUCXX) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra") + if (NOT ${COMPILER_IS_NVCC}) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pedantic-errors") + endif() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g") +endif() + +############################################################################### +# BACKWARD OBJECT +############################################################################### + +add_library(backward_object OBJECT backward.cpp) +target_compile_definitions(backward_object PRIVATE ${BACKWARD_DEFINITIONS}) +target_include_directories(backward_object PRIVATE ${BACKWARD_INCLUDE_DIRS}) +set(BACKWARD_ENABLE $ CACHE STRING + "Link with this object to setup backward automatically") + + +############################################################################### +# BACKWARD LIBRARY (Includes backward.cpp) +############################################################################### +option(BACKWARD_SHARED "Build dynamic backward-cpp shared lib" OFF) + +if(BACKWARD_SHARED) + set(libtype SHARED) +endif() +add_library(backward ${libtype} backward.cpp) +target_compile_definitions(backward PUBLIC ${BACKWARD_DEFINITIONS}) +target_include_directories(backward PUBLIC ${BACKWARD_INCLUDE_DIRS}) + +############################################################################### +# TESTS +############################################################################### + +if(BACKWARD_TESTS) + enable_testing() + + add_library(test_main SHARED test/_test_main.cpp) + + macro(backward_add_test src) + get_filename_component(name ${src} NAME_WE) + set(test_name "test_${name}") + + add_executable(${test_name} ${src} ${ARGN}) + + target_link_libraries(${test_name} PRIVATE Backward::Backward test_main) + + add_test(NAME ${name} COMMAND ${test_name}) + endmacro() + + # Tests without backward.cpp + set(TESTS + test + stacktrace + rectrace + select_signals + ) + + foreach(test ${TESTS}) + backward_add_test(test/${test}.cpp) + endforeach() + + # Tests with backward.cpp + set(TESTS + suicide + ) + + foreach(test ${TESTS}) + backward_add_test(test/${test}.cpp ${BACKWARD_ENABLE}) + endforeach() +endif() + +install( + FILES "backward.hpp" + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) +install( + FILES "BackwardConfig.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/backward +) diff --git a/external/backward-cpp/LICENSE.txt b/external/backward-cpp/LICENSE.txt new file mode 100644 index 0000000..269e8ab --- /dev/null +++ b/external/backward-cpp/LICENSE.txt @@ -0,0 +1,21 @@ +Copyright 2013 Google Inc. All Rights Reserved. + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/external/backward-cpp/backward.cpp b/external/backward-cpp/backward.cpp new file mode 100644 index 0000000..4c68284 --- /dev/null +++ b/external/backward-cpp/backward.cpp @@ -0,0 +1,32 @@ +// Pick your poison. +// +// On GNU/Linux, you have few choices to get the most out of your stack trace. +// +// By default you get: +// - object filename +// - function name +// +// In order to add: +// - source filename +// - line and column numbers +// - source code snippet (assuming the file is accessible) + +// Install one of the following library then uncomment one of the macro (or +// better, add the detection of the lib and the macro definition in your build +// system) + +// - apt-get install libdw-dev ... +// - g++/clang++ -ldw ... +// #define BACKWARD_HAS_DW 1 + +// - apt-get install binutils-dev ... +// - g++/clang++ -lbfd ... +// #define BACKWARD_HAS_BFD 1 + +#include "backward.hpp" + +namespace backward { + +backward::SignalHandling sh; + +} // namespace backward diff --git a/external/backward-cpp/backward.hpp b/external/backward-cpp/backward.hpp new file mode 100644 index 0000000..69d5dba --- /dev/null +++ b/external/backward-cpp/backward.hpp @@ -0,0 +1,3800 @@ +/* + * backward.hpp + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef H_6B9572DA_A64B_49E6_B234_051480991C89 +#define H_6B9572DA_A64B_49E6_B234_051480991C89 + +#ifndef __cplusplus +# error "It's not going to compile without a C++ compiler..." +#endif + +#if defined(BACKWARD_CXX11) +#elif defined(BACKWARD_CXX98) +#else +# if __cplusplus >= 201103L +# define BACKWARD_CXX11 +# define BACKWARD_ATLEAST_CXX11 +# define BACKWARD_ATLEAST_CXX98 +# else +# define BACKWARD_CXX98 +# define BACKWARD_ATLEAST_CXX98 +# endif +#endif + +// You can define one of the following (or leave it to the auto-detection): +// +// #define BACKWARD_SYSTEM_LINUX +// - specialization for linux +// +// #define BACKWARD_SYSTEM_DARWIN +// - specialization for Mac OS X 10.5 and later. +// +// #define BACKWARD_SYSTEM_UNKNOWN +// - placebo implementation, does nothing. +// +#if defined(BACKWARD_SYSTEM_LINUX) +#elif defined(BACKWARD_SYSTEM_DARWIN) +#elif defined(BACKWARD_SYSTEM_UNKNOWN) +#else +# if defined(__linux) || defined(__linux__) +# define BACKWARD_SYSTEM_LINUX +# elif defined(__APPLE__) +# define BACKWARD_SYSTEM_DARWIN +# else +# define BACKWARD_SYSTEM_UNKNOWN +# endif +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(BACKWARD_SYSTEM_LINUX) + +// On linux, backtrace can back-trace or "walk" the stack using the following +// libraries: +// +// #define BACKWARD_HAS_UNWIND 1 +// - unwind comes from libgcc, but I saw an equivalent inside clang itself. +// - with unwind, the stacktrace is as accurate as it can possibly be, since +// this is used by the C++ runtine in gcc/clang for stack unwinding on +// exception. +// - normally libgcc is already linked to your program by default. +// +// #define BACKWARD_HAS_BACKTRACE == 1 +// - backtrace seems to be a little bit more portable than libunwind, but on +// linux, it uses unwind anyway, but abstract away a tiny information that is +// sadly really important in order to get perfectly accurate stack traces. +// - backtrace is part of the (e)glib library. +// +// The default is: +// #define BACKWARD_HAS_UNWIND == 1 +// +// Note that only one of the define should be set to 1 at a time. +// +# if BACKWARD_HAS_UNWIND == 1 +# elif BACKWARD_HAS_BACKTRACE == 1 +# else +# undef BACKWARD_HAS_UNWIND +# define BACKWARD_HAS_UNWIND 1 +# undef BACKWARD_HAS_BACKTRACE +# define BACKWARD_HAS_BACKTRACE 0 +# endif + +// On linux, backward can extract detailed information about a stack trace +// using one of the following libraries: +// +// #define BACKWARD_HAS_DW 1 +// - libdw gives you the most juicy details out of your stack traces: +// - object filename +// - function name +// - source filename +// - line and column numbers +// - source code snippet (assuming the file is accessible) +// - variables name and values (if not optimized out) +// - You need to link with the lib "dw": +// - apt-get install libdw-dev +// - g++/clang++ -ldw ... +// +// #define BACKWARD_HAS_BFD 1 +// - With libbfd, you get a fair amount of details: +// - object filename +// - function name +// - source filename +// - line numbers +// - source code snippet (assuming the file is accessible) +// - You need to link with the lib "bfd": +// - apt-get install binutils-dev +// - g++/clang++ -lbfd ... +// +// #define BACKWARD_HAS_DWARF 1 +// - libdwarf gives you the most juicy details out of your stack traces: +// - object filename +// - function name +// - source filename +// - line and column numbers +// - source code snippet (assuming the file is accessible) +// - variables name and values (if not optimized out) +// - You need to link with the lib "dwarf": +// - apt-get install libdwarf-dev +// - g++/clang++ -ldwarf ... +// +// #define BACKWARD_HAS_BACKTRACE_SYMBOL 1 +// - backtrace provides minimal details for a stack trace: +// - object filename +// - function name +// - backtrace is part of the (e)glib library. +// +// The default is: +// #define BACKWARD_HAS_BACKTRACE_SYMBOL == 1 +// +// Note that only one of the define should be set to 1 at a time. +// +# if BACKWARD_HAS_DW == 1 +# elif BACKWARD_HAS_BFD == 1 +# elif BACKWARD_HAS_DWARF == 1 +# elif BACKWARD_HAS_BACKTRACE_SYMBOL == 1 +# else +# undef BACKWARD_HAS_DW +# define BACKWARD_HAS_DW 0 +# undef BACKWARD_HAS_BFD +# define BACKWARD_HAS_BFD 0 +# undef BACKWARD_HAS_DWARF +# define BACKWARD_HAS_DWARF 0 +# undef BACKWARD_HAS_BACKTRACE_SYMBOL +# define BACKWARD_HAS_BACKTRACE_SYMBOL 1 +# endif + +# include +# include +# ifdef __ANDROID__ +// Old Android API levels define _Unwind_Ptr in both link.h and unwind.h +// Rename the one in link.h as we are not going to be using it +# define _Unwind_Ptr _Unwind_Ptr_Custom +# include +# undef _Unwind_Ptr +# else +# include +# endif +# include +# include +# include +# include + +# if BACKWARD_HAS_BFD == 1 +// NOTE: defining PACKAGE{,_VERSION} is required before including +// bfd.h on some platforms, see also: +// https://sourceware.org/bugzilla/show_bug.cgi?id=14243 +# ifndef PACKAGE +# define PACKAGE +# endif +# ifndef PACKAGE_VERSION +# define PACKAGE_VERSION +# endif +# include +# ifndef _GNU_SOURCE +# define _GNU_SOURCE +# include +# undef _GNU_SOURCE +# else +# include +# endif +# endif + +# if BACKWARD_HAS_DW == 1 +# include +# include +# include +# endif + +# if BACKWARD_HAS_DWARF == 1 +# include +# include +# include +# include +# include +# ifndef _GNU_SOURCE +# define _GNU_SOURCE +# include +# undef _GNU_SOURCE +# else +# include +# endif +# endif + +# if (BACKWARD_HAS_BACKTRACE == 1) || (BACKWARD_HAS_BACKTRACE_SYMBOL == 1) + // then we shall rely on backtrace +# include +# endif + +#endif // defined(BACKWARD_SYSTEM_LINUX) + +#if defined(BACKWARD_SYSTEM_DARWIN) +// On Darwin, backtrace can back-trace or "walk" the stack using the following +// libraries: +// +// #define BACKWARD_HAS_UNWIND 1 +// - unwind comes from libgcc, but I saw an equivalent inside clang itself. +// - with unwind, the stacktrace is as accurate as it can possibly be, since +// this is used by the C++ runtine in gcc/clang for stack unwinding on +// exception. +// - normally libgcc is already linked to your program by default. +// +// #define BACKWARD_HAS_BACKTRACE == 1 +// - backtrace is available by default, though it does not produce as much information +// as another library might. +// +// The default is: +// #define BACKWARD_HAS_UNWIND == 1 +// +// Note that only one of the define should be set to 1 at a time. +// +# if BACKWARD_HAS_UNWIND == 1 +# elif BACKWARD_HAS_BACKTRACE == 1 +# else +# undef BACKWARD_HAS_UNWIND +# define BACKWARD_HAS_UNWIND 1 +# undef BACKWARD_HAS_BACKTRACE +# define BACKWARD_HAS_BACKTRACE 0 +# endif + +// On Darwin, backward can extract detailed information about a stack trace +// using one of the following libraries: +// +// #define BACKWARD_HAS_BACKTRACE_SYMBOL 1 +// - backtrace provides minimal details for a stack trace: +// - object filename +// - function name +// +// The default is: +// #define BACKWARD_HAS_BACKTRACE_SYMBOL == 1 +// +# if BACKWARD_HAS_BACKTRACE_SYMBOL == 1 +# else +# undef BACKWARD_HAS_BACKTRACE_SYMBOL +# define BACKWARD_HAS_BACKTRACE_SYMBOL 1 +# endif + +# include +# include +# include +# include +# include +# include + +# if (BACKWARD_HAS_BACKTRACE == 1) || (BACKWARD_HAS_BACKTRACE_SYMBOL == 1) +# include +# endif +#endif // defined(BACKWARD_SYSTEM_DARWIN) + +#if BACKWARD_HAS_UNWIND == 1 + +# include +// while gcc's unwind.h defines something like that: +// extern _Unwind_Ptr _Unwind_GetIP (struct _Unwind_Context *); +// extern _Unwind_Ptr _Unwind_GetIPInfo (struct _Unwind_Context *, int *); +// +// clang's unwind.h defines something like this: +// uintptr_t _Unwind_GetIP(struct _Unwind_Context* __context); +// +// Even if the _Unwind_GetIPInfo can be linked to, it is not declared, worse we +// cannot just redeclare it because clang's unwind.h doesn't define _Unwind_Ptr +// anyway. +// +// Luckily we can play on the fact that the guard macros have a different name: +#ifdef __CLANG_UNWIND_H +// In fact, this function still comes from libgcc (on my different linux boxes, +// clang links against libgcc). +# include +extern "C" uintptr_t _Unwind_GetIPInfo(_Unwind_Context*, int*); +#endif + +#endif // BACKWARD_HAS_UNWIND == 1 + +#ifdef BACKWARD_ATLEAST_CXX11 +# include +# include // for std::swap + namespace backward { + namespace details { + template + struct hashtable { + typedef std::unordered_map type; + }; + using std::move; + } // namespace details + } // namespace backward +#else // NOT BACKWARD_ATLEAST_CXX11 +# define nullptr NULL +# define override +# include + namespace backward { + namespace details { + template + struct hashtable { + typedef std::map type; + }; + template + const T& move(const T& v) { return v; } + template + T& move(T& v) { return v; } + } // namespace details + } // namespace backward +#endif // BACKWARD_ATLEAST_CXX11 + +namespace backward { + +namespace system_tag { + struct linux_tag; // seems that I cannot call that "linux" because the name + // is already defined... so I am adding _tag everywhere. + struct darwin_tag; + struct unknown_tag; + +#if defined(BACKWARD_SYSTEM_LINUX) + typedef linux_tag current_tag; +#elif defined(BACKWARD_SYSTEM_DARWIN) + typedef darwin_tag current_tag; +#elif defined(BACKWARD_SYSTEM_UNKNOWN) + typedef unknown_tag current_tag; +#else +# error "May I please get my system defines?" +#endif +} // namespace system_tag + + +namespace trace_resolver_tag { +#if defined(BACKWARD_SYSTEM_LINUX) + struct libdw; + struct libbfd; + struct libdwarf; + struct backtrace_symbol; + +# if BACKWARD_HAS_DW == 1 + typedef libdw current; +# elif BACKWARD_HAS_BFD == 1 + typedef libbfd current; +# elif BACKWARD_HAS_DWARF == 1 + typedef libdwarf current; +# elif BACKWARD_HAS_BACKTRACE_SYMBOL == 1 + typedef backtrace_symbol current; +# else +# error "You shall not pass, until you know what you want." +# endif +#elif defined(BACKWARD_SYSTEM_DARWIN) + struct backtrace_symbol; + +# if BACKWARD_HAS_BACKTRACE_SYMBOL == 1 + typedef backtrace_symbol current; +# else +# error "You shall not pass, until you know what you want." +# endif +#endif +} // namespace trace_resolver_tag + + +namespace details { + +template + struct rm_ptr { typedef T type; }; + +template + struct rm_ptr { typedef T type; }; + +template + struct rm_ptr { typedef const T type; }; + +template +struct deleter { + template + void operator()(U& ptr) const { + (*F)(ptr); + } +}; + +template +struct default_delete { + void operator()(T& ptr) const { + delete ptr; + } +}; + +template > +class handle { + struct dummy; + T _val; + bool _empty; + +#ifdef BACKWARD_ATLEAST_CXX11 + handle(const handle&) = delete; + handle& operator=(const handle&) = delete; +#endif + +public: + ~handle() { + if (!_empty) { + Deleter()(_val); + } + } + + explicit handle(): _val(), _empty(true) {} + explicit handle(T val): _val(val), _empty(false) { if(!_val) _empty = true; } + +#ifdef BACKWARD_ATLEAST_CXX11 + handle(handle&& from): _empty(true) { + swap(from); + } + handle& operator=(handle&& from) { + swap(from); return *this; + } +#else + explicit handle(const handle& from): _empty(true) { + // some sort of poor man's move semantic. + swap(const_cast(from)); + } + handle& operator=(const handle& from) { + // some sort of poor man's move semantic. + swap(const_cast(from)); return *this; + } +#endif + + void reset(T new_val) { + handle tmp(new_val); + swap(tmp); + } + operator const dummy*() const { + if (_empty) { + return nullptr; + } + return reinterpret_cast(_val); + } + T get() { + return _val; + } + T release() { + _empty = true; + return _val; + } + void swap(handle& b) { + using std::swap; + swap(b._val, _val); // can throw, we are safe here. + swap(b._empty, _empty); // should not throw: if you cannot swap two + // bools without throwing... It's a lost cause anyway! + } + + T operator->() { return _val; } + const T operator->() const { return _val; } + + typedef typename rm_ptr::type& ref_t; + typedef const typename rm_ptr::type& const_ref_t; + ref_t operator*() { return *_val; } + const_ref_t operator*() const { return *_val; } + ref_t operator[](size_t idx) { return _val[idx]; } + + // Watch out, we've got a badass over here + T* operator&() { + _empty = false; + return &_val; + } +}; + +// Default demangler implementation (do nothing). +template +struct demangler_impl { + static std::string demangle(const char* funcname) { + return funcname; + } +}; + +#if defined(BACKWARD_SYSTEM_LINUX) || defined(BACKWARD_SYSTEM_DARWIN) + +template <> +struct demangler_impl { + demangler_impl(): _demangle_buffer_length(0) {} + + std::string demangle(const char* funcname) { + using namespace details; + char* result = abi::__cxa_demangle(funcname, + _demangle_buffer.release(), &_demangle_buffer_length, nullptr); + if(result) { + _demangle_buffer.reset(result); + return result; + } + return funcname; + } + +private: + details::handle _demangle_buffer; + size_t _demangle_buffer_length; +}; + +#endif // BACKWARD_SYSTEM_LINUX || BACKWARD_SYSTEM_DARWIN + +struct demangler: + public demangler_impl {}; + +} // namespace details + +/*************** A TRACE ***************/ + +struct Trace { + void* addr; + size_t idx; + + Trace(): + addr(nullptr), idx(0) {} + + explicit Trace(void* _addr, size_t _idx): + addr(_addr), idx(_idx) {} +}; + +struct ResolvedTrace: public Trace { + + struct SourceLoc { + std::string function; + std::string filename; + unsigned line; + unsigned col; + + SourceLoc(): line(0), col(0) {} + + bool operator==(const SourceLoc& b) const { + return function == b.function + && filename == b.filename + && line == b.line + && col == b.col; + } + + bool operator!=(const SourceLoc& b) const { + return !(*this == b); + } + }; + + // In which binary object this trace is located. + std::string object_filename; + + // The function in the object that contain the trace. This is not the same + // as source.function which can be an function inlined in object_function. + std::string object_function; + + // The source location of this trace. It is possible for filename to be + // empty and for line/col to be invalid (value 0) if this information + // couldn't be deduced, for example if there is no debug information in the + // binary object. + SourceLoc source; + + // An optionals list of "inliners". All the successive sources location + // from where the source location of the trace (the attribute right above) + // is inlined. It is especially useful when you compiled with optimization. + typedef std::vector source_locs_t; + source_locs_t inliners; + + ResolvedTrace(): + Trace() {} + ResolvedTrace(const Trace& mini_trace): + Trace(mini_trace) {} +}; + +/*************** STACK TRACE ***************/ + +// default implemention. +template +class StackTraceImpl { +public: + size_t size() const { return 0; } + Trace operator[](size_t) { return Trace(); } + size_t load_here(size_t=0) { return 0; } + size_t load_from(void*, size_t=0) { return 0; } + size_t thread_id() const { return 0; } + void skip_n_firsts(size_t) { } +}; + +class StackTraceImplBase { +public: + StackTraceImplBase(): _thread_id(0), _skip(0) {} + + size_t thread_id() const { + return _thread_id; + } + + void skip_n_firsts(size_t n) { _skip = n; } + +protected: + void load_thread_info() { +#ifdef BACKWARD_SYSTEM_LINUX +#ifndef __ANDROID__ + _thread_id = static_cast(syscall(SYS_gettid)); +#else + _thread_id = static_cast(gettid()); +#endif + if (_thread_id == static_cast(getpid())) { + // If the thread is the main one, let's hide that. + // I like to keep little secret sometimes. + _thread_id = 0; + } +#elif defined(BACKWARD_SYSTEM_DARWIN) + _thread_id = reinterpret_cast(pthread_self()); + if (pthread_main_np() == 1) { + // If the thread is the main one, let's hide that. + _thread_id = 0; + } +#endif + } + + size_t skip_n_firsts() const { return _skip; } + +private: + size_t _thread_id; + size_t _skip; +}; + +class StackTraceImplHolder: public StackTraceImplBase { +public: + size_t size() const { + return _stacktrace.size() ? _stacktrace.size() - skip_n_firsts() : 0; + } + Trace operator[](size_t idx) const { + if (idx >= size()) { + return Trace(); + } + return Trace(_stacktrace[idx + skip_n_firsts()], idx); + } + void* const* begin() const { + if (size()) { + return &_stacktrace[skip_n_firsts()]; + } + return nullptr; + } + +protected: + std::vector _stacktrace; +}; + + +#if BACKWARD_HAS_UNWIND == 1 + +namespace details { + +template +class Unwinder { +public: + size_t operator()(F& f, size_t depth) { + _f = &f; + _index = -1; + _depth = depth; + _Unwind_Backtrace(&this->backtrace_trampoline, this); + return static_cast(_index); + } + +private: + F* _f; + ssize_t _index; + size_t _depth; + + static _Unwind_Reason_Code backtrace_trampoline( + _Unwind_Context* ctx, void *self) { + return (static_cast(self))->backtrace(ctx); + } + + _Unwind_Reason_Code backtrace(_Unwind_Context* ctx) { + if (_index >= 0 && static_cast(_index) >= _depth) + return _URC_END_OF_STACK; + + int ip_before_instruction = 0; + uintptr_t ip = _Unwind_GetIPInfo(ctx, &ip_before_instruction); + + if (!ip_before_instruction) { + // calculating 0-1 for unsigned, looks like a possible bug to sanitiziers, so let's do it explicitly: + if (ip==0) { + ip = std::numeric_limits::max(); // set it to 0xffff... (as from casting 0-1) + } else { + ip -= 1; // else just normally decrement it (no overflow/underflow will happen) + } + } + + if (_index >= 0) { // ignore first frame. + (*_f)(static_cast(_index), reinterpret_cast(ip)); + } + _index += 1; + return _URC_NO_REASON; + } +}; + +template +size_t unwind(F f, size_t depth) { + Unwinder unwinder; + return unwinder(f, depth); +} + +} // namespace details + + +template <> +class StackTraceImpl: public StackTraceImplHolder { +public: + __attribute__ ((noinline)) // TODO use some macro + size_t load_here(size_t depth=32) { + load_thread_info(); + if (depth == 0) { + return 0; + } + _stacktrace.resize(depth); + size_t trace_cnt = details::unwind(callback(*this), depth); + _stacktrace.resize(trace_cnt); + skip_n_firsts(0); + return size(); + } + size_t load_from(void* addr, size_t depth=32) { + load_here(depth + 8); + + for (size_t i = 0; i < _stacktrace.size(); ++i) { + if (_stacktrace[i] == addr) { + skip_n_firsts(i); + break; + } + } + + _stacktrace.resize(std::min(_stacktrace.size(), + skip_n_firsts() + depth)); + return size(); + } + +private: + struct callback { + StackTraceImpl& self; + callback(StackTraceImpl& _self): self(_self) {} + + void operator()(size_t idx, void* addr) { + self._stacktrace[idx] = addr; + } + }; +}; + + +#else // BACKWARD_HAS_UNWIND == 0 + +template <> +class StackTraceImpl: public StackTraceImplHolder { +public: + __attribute__ ((noinline)) // TODO use some macro + size_t load_here(size_t depth=32) { + load_thread_info(); + if (depth == 0) { + return 0; + } + _stacktrace.resize(depth + 1); + size_t trace_cnt = backtrace(&_stacktrace[0], _stacktrace.size()); + _stacktrace.resize(trace_cnt); + skip_n_firsts(1); + return size(); + } + + size_t load_from(void* addr, size_t depth=32) { + load_here(depth + 8); + + for (size_t i = 0; i < _stacktrace.size(); ++i) { + if (_stacktrace[i] == addr) { + skip_n_firsts(i); + _stacktrace[i] = (void*)( (uintptr_t)_stacktrace[i] + 1); + break; + } + } + + _stacktrace.resize(std::min(_stacktrace.size(), + skip_n_firsts() + depth)); + return size(); + } +}; + +#endif // BACKWARD_HAS_UNWIND + +class StackTrace: + public StackTraceImpl {}; + +/*************** TRACE RESOLVER ***************/ + +template +class TraceResolverImpl; + +#ifdef BACKWARD_SYSTEM_UNKNOWN + +template <> +class TraceResolverImpl { +public: + template + void load_stacktrace(ST&) {} + ResolvedTrace resolve(ResolvedTrace t) { + return t; + } +}; + +#endif + +class TraceResolverImplBase { +protected: + std::string demangle(const char* funcname) { + return _demangler.demangle(funcname); + } + +private: + details::demangler _demangler; +}; + +#ifdef BACKWARD_SYSTEM_LINUX + +template +class TraceResolverLinuxImpl; + +#if BACKWARD_HAS_BACKTRACE_SYMBOL == 1 + +template <> +class TraceResolverLinuxImpl: + public TraceResolverImplBase { +public: + template + void load_stacktrace(ST& st) { + using namespace details; + if (st.size() == 0) { + return; + } + _symbols.reset( + backtrace_symbols(st.begin(), (int)st.size()) + ); + } + + ResolvedTrace resolve(ResolvedTrace trace) { + char* filename = _symbols[trace.idx]; + char* funcname = filename; + while (*funcname && *funcname != '(') { + funcname += 1; + } + trace.object_filename.assign(filename, funcname); // ok even if funcname is the ending \0 (then we assign entire string) + + if (*funcname) { // if it's not end of string (e.g. from last frame ip==0) + funcname += 1; + char* funcname_end = funcname; + while (*funcname_end && *funcname_end != ')' && *funcname_end != '+') { + funcname_end += 1; + } + *funcname_end = '\0'; + trace.object_function = this->demangle(funcname); + trace.source.function = trace.object_function; // we cannot do better. + } + return trace; + } + +private: + details::handle _symbols; +}; + +#endif // BACKWARD_HAS_BACKTRACE_SYMBOL == 1 + +#if BACKWARD_HAS_BFD == 1 + +template <> +class TraceResolverLinuxImpl: + public TraceResolverImplBase { + static std::string read_symlink(std::string const & symlink_path) { + std::string path; + path.resize(100); + + while(true) { + ssize_t len = ::readlink(symlink_path.c_str(), &*path.begin(), path.size()); + if(len < 0) { + return ""; + } + if (static_cast(len) == path.size()) { + path.resize(path.size() * 2); + } + else { + path.resize(static_cast(len)); + break; + } + } + + return path; + } +public: + TraceResolverLinuxImpl(): _bfd_loaded(false) {} + + template + void load_stacktrace(ST&) {} + + ResolvedTrace resolve(ResolvedTrace trace) { + Dl_info symbol_info; + + // trace.addr is a virtual address in memory pointing to some code. + // Let's try to find from which loaded object it comes from. + // The loaded object can be yourself btw. + if (!dladdr(trace.addr, &symbol_info)) { + return trace; // dat broken trace... + } + + std::string argv0; + { + std::ifstream ifs("/proc/self/cmdline"); + std::getline(ifs, argv0, '\0'); + } + std::string tmp; + if(symbol_info.dli_fname == argv0) { + tmp = read_symlink("/proc/self/exe"); + symbol_info.dli_fname = tmp.c_str(); + } + + // Now we get in symbol_info: + // .dli_fname: + // pathname of the shared object that contains the address. + // .dli_fbase: + // where the object is loaded in memory. + // .dli_sname: + // the name of the nearest symbol to trace.addr, we expect a + // function name. + // .dli_saddr: + // the exact address corresponding to .dli_sname. + + if (symbol_info.dli_sname) { + trace.object_function = demangle(symbol_info.dli_sname); + } + + if (!symbol_info.dli_fname) { + return trace; + } + + trace.object_filename = symbol_info.dli_fname; + bfd_fileobject& fobj = load_object_with_bfd(symbol_info.dli_fname); + if (!fobj.handle) { + return trace; // sad, we couldn't load the object :( + } + + + find_sym_result* details_selected; // to be filled. + + // trace.addr is the next instruction to be executed after returning + // from the nested stack frame. In C++ this usually relate to the next + // statement right after the function call that leaded to a new stack + // frame. This is not usually what you want to see when printing out a + // stacktrace... + find_sym_result details_call_site = find_symbol_details(fobj, + trace.addr, symbol_info.dli_fbase); + details_selected = &details_call_site; + +#if BACKWARD_HAS_UNWIND == 0 + // ...this is why we also try to resolve the symbol that is right + // before the return address. If we are lucky enough, we will get the + // line of the function that was called. But if the code is optimized, + // we might get something absolutely not related since the compiler + // can reschedule the return address with inline functions and + // tail-call optimisation (among other things that I don't even know + // or cannot even dream about with my tiny limited brain). + find_sym_result details_adjusted_call_site = find_symbol_details(fobj, + (void*) (uintptr_t(trace.addr) - 1), + symbol_info.dli_fbase); + + // In debug mode, we should always get the right thing(TM). + if (details_call_site.found && details_adjusted_call_site.found) { + // Ok, we assume that details_adjusted_call_site is a better estimation. + details_selected = &details_adjusted_call_site; + trace.addr = (void*) (uintptr_t(trace.addr) - 1); + } + + if (details_selected == &details_call_site && details_call_site.found) { + // we have to re-resolve the symbol in order to reset some + // internal state in BFD... so we can call backtrace_inliners + // thereafter... + details_call_site = find_symbol_details(fobj, trace.addr, + symbol_info.dli_fbase); + } +#endif // BACKWARD_HAS_UNWIND + + if (details_selected->found) { + if (details_selected->filename) { + trace.source.filename = details_selected->filename; + } + trace.source.line = details_selected->line; + + if (details_selected->funcname) { + // this time we get the name of the function where the code is + // located, instead of the function were the address is + // located. In short, if the code was inlined, we get the + // function correspoding to the code. Else we already got in + // trace.function. + trace.source.function = demangle(details_selected->funcname); + + if (!symbol_info.dli_sname) { + // for the case dladdr failed to find the symbol name of + // the function, we might as well try to put something + // here. + trace.object_function = trace.source.function; + } + } + + // Maybe the source of the trace got inlined inside the function + // (trace.source.function). Let's see if we can get all the inlined + // calls along the way up to the initial call site. + trace.inliners = backtrace_inliners(fobj, *details_selected); + +#if 0 + if (trace.inliners.size() == 0) { + // Maybe the trace was not inlined... or maybe it was and we + // are lacking the debug information. Let's try to make the + // world better and see if we can get the line number of the + // function (trace.source.function) now. + // + // We will get the location of where the function start (to be + // exact: the first instruction that really start the + // function), not where the name of the function is defined. + // This can be quite far away from the name of the function + // btw. + // + // If the source of the function is the same as the source of + // the trace, we cannot say if the trace was really inlined or + // not. However, if the filename of the source is different + // between the function and the trace... we can declare it as + // an inliner. This is not 100% accurate, but better than + // nothing. + + if (symbol_info.dli_saddr) { + find_sym_result details = find_symbol_details(fobj, + symbol_info.dli_saddr, + symbol_info.dli_fbase); + + if (details.found) { + ResolvedTrace::SourceLoc diy_inliner; + diy_inliner.line = details.line; + if (details.filename) { + diy_inliner.filename = details.filename; + } + if (details.funcname) { + diy_inliner.function = demangle(details.funcname); + } else { + diy_inliner.function = trace.source.function; + } + if (diy_inliner != trace.source) { + trace.inliners.push_back(diy_inliner); + } + } + } + } +#endif + } + + return trace; + } + +private: + bool _bfd_loaded; + + typedef details::handle + > bfd_handle_t; + + typedef details::handle bfd_symtab_t; + + + struct bfd_fileobject { + bfd_handle_t handle; + bfd_vma base_addr; + bfd_symtab_t symtab; + bfd_symtab_t dynamic_symtab; + }; + + typedef details::hashtable::type + fobj_bfd_map_t; + fobj_bfd_map_t _fobj_bfd_map; + + bfd_fileobject& load_object_with_bfd(const std::string& filename_object) { + using namespace details; + + if (!_bfd_loaded) { + using namespace details; + bfd_init(); + _bfd_loaded = true; + } + + fobj_bfd_map_t::iterator it = + _fobj_bfd_map.find(filename_object); + if (it != _fobj_bfd_map.end()) { + return it->second; + } + + // this new object is empty for now. + bfd_fileobject& r = _fobj_bfd_map[filename_object]; + + // we do the work temporary in this one; + bfd_handle_t bfd_handle; + + int fd = open(filename_object.c_str(), O_RDONLY); + bfd_handle.reset( + bfd_fdopenr(filename_object.c_str(), "default", fd) + ); + if (!bfd_handle) { + close(fd); + return r; + } + + if (!bfd_check_format(bfd_handle.get(), bfd_object)) { + return r; // not an object? You lose. + } + + if ((bfd_get_file_flags(bfd_handle.get()) & HAS_SYMS) == 0) { + return r; // that's what happen when you forget to compile in debug. + } + + ssize_t symtab_storage_size = + bfd_get_symtab_upper_bound(bfd_handle.get()); + + ssize_t dyn_symtab_storage_size = + bfd_get_dynamic_symtab_upper_bound(bfd_handle.get()); + + if (symtab_storage_size <= 0 && dyn_symtab_storage_size <= 0) { + return r; // weird, is the file is corrupted? + } + + bfd_symtab_t symtab, dynamic_symtab; + ssize_t symcount = 0, dyn_symcount = 0; + + if (symtab_storage_size > 0) { + symtab.reset( + static_cast(malloc(static_cast(symtab_storage_size))) + ); + symcount = bfd_canonicalize_symtab( + bfd_handle.get(), symtab.get() + ); + } + + if (dyn_symtab_storage_size > 0) { + dynamic_symtab.reset( + static_cast(malloc(static_cast(dyn_symtab_storage_size))) + ); + dyn_symcount = bfd_canonicalize_dynamic_symtab( + bfd_handle.get(), dynamic_symtab.get() + ); + } + + + if (symcount <= 0 && dyn_symcount <= 0) { + return r; // damned, that's a stripped file that you got there! + } + + r.handle = move(bfd_handle); + r.symtab = move(symtab); + r.dynamic_symtab = move(dynamic_symtab); + return r; + } + + struct find_sym_result { + bool found; + const char* filename; + const char* funcname; + unsigned int line; + }; + + struct find_sym_context { + TraceResolverLinuxImpl* self; + bfd_fileobject* fobj; + void* addr; + void* base_addr; + find_sym_result result; + }; + + find_sym_result find_symbol_details(bfd_fileobject& fobj, void* addr, + void* base_addr) { + find_sym_context context; + context.self = this; + context.fobj = &fobj; + context.addr = addr; + context.base_addr = base_addr; + context.result.found = false; + bfd_map_over_sections(fobj.handle.get(), &find_in_section_trampoline, + static_cast(&context)); + return context.result; + } + + static void find_in_section_trampoline(bfd*, asection* section, + void* data) { + find_sym_context* context = static_cast(data); + context->self->find_in_section( + reinterpret_cast(context->addr), + reinterpret_cast(context->base_addr), + *context->fobj, + section, context->result + ); + } + + void find_in_section(bfd_vma addr, bfd_vma base_addr, + bfd_fileobject& fobj, asection* section, find_sym_result& result) + { + if (result.found) return; + + if ((bfd_get_section_flags(fobj.handle.get(), section) + & SEC_ALLOC) == 0) + return; // a debug section is never loaded automatically. + + bfd_vma sec_addr = bfd_get_section_vma(fobj.handle.get(), section); + bfd_size_type size = bfd_get_section_size(section); + + // are we in the boundaries of the section? + if (addr < sec_addr || addr >= sec_addr + size) { + addr -= base_addr; // oups, a relocated object, lets try again... + if (addr < sec_addr || addr >= sec_addr + size) { + return; + } + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" + if (!result.found && fobj.symtab) { + result.found = bfd_find_nearest_line(fobj.handle.get(), section, + fobj.symtab.get(), addr - sec_addr, &result.filename, + &result.funcname, &result.line); + } + + if (!result.found && fobj.dynamic_symtab) { + result.found = bfd_find_nearest_line(fobj.handle.get(), section, + fobj.dynamic_symtab.get(), addr - sec_addr, + &result.filename, &result.funcname, &result.line); + } +#pragma clang diagnostic pop + + } + + ResolvedTrace::source_locs_t backtrace_inliners(bfd_fileobject& fobj, + find_sym_result previous_result) { + // This function can be called ONLY after a SUCCESSFUL call to + // find_symbol_details. The state is global to the bfd_handle. + ResolvedTrace::source_locs_t results; + while (previous_result.found) { + find_sym_result result; + result.found = bfd_find_inliner_info(fobj.handle.get(), + &result.filename, &result.funcname, &result.line); + + if (result.found) /* and not ( + cstrings_eq(previous_result.filename, result.filename) + and cstrings_eq(previous_result.funcname, result.funcname) + and result.line == previous_result.line + )) */ { + ResolvedTrace::SourceLoc src_loc; + src_loc.line = result.line; + if (result.filename) { + src_loc.filename = result.filename; + } + if (result.funcname) { + src_loc.function = demangle(result.funcname); + } + results.push_back(src_loc); + } + previous_result = result; + } + return results; + } + + bool cstrings_eq(const char* a, const char* b) { + if (!a || !b) { + return false; + } + return strcmp(a, b) == 0; + } + +}; +#endif // BACKWARD_HAS_BFD == 1 + +#if BACKWARD_HAS_DW == 1 + +template <> +class TraceResolverLinuxImpl: + public TraceResolverImplBase { +public: + TraceResolverLinuxImpl(): _dwfl_handle_initialized(false) {} + + template + void load_stacktrace(ST&) {} + + ResolvedTrace resolve(ResolvedTrace trace) { + using namespace details; + + Dwarf_Addr trace_addr = (Dwarf_Addr) trace.addr; + + if (!_dwfl_handle_initialized) { + // initialize dwfl... + _dwfl_cb.reset(new Dwfl_Callbacks); + _dwfl_cb->find_elf = &dwfl_linux_proc_find_elf; + _dwfl_cb->find_debuginfo = &dwfl_standard_find_debuginfo; + _dwfl_cb->debuginfo_path = 0; + + _dwfl_handle.reset(dwfl_begin(_dwfl_cb.get())); + _dwfl_handle_initialized = true; + + if (!_dwfl_handle) { + return trace; + } + + // ...from the current process. + dwfl_report_begin(_dwfl_handle.get()); + int r = dwfl_linux_proc_report (_dwfl_handle.get(), getpid()); + dwfl_report_end(_dwfl_handle.get(), NULL, NULL); + if (r < 0) { + return trace; + } + } + + if (!_dwfl_handle) { + return trace; + } + + // find the module (binary object) that contains the trace's address. + // This is not using any debug information, but the addresses ranges of + // all the currently loaded binary object. + Dwfl_Module* mod = dwfl_addrmodule(_dwfl_handle.get(), trace_addr); + if (mod) { + // now that we found it, lets get the name of it, this will be the + // full path to the running binary or one of the loaded library. + const char* module_name = dwfl_module_info (mod, + 0, 0, 0, 0, 0, 0, 0); + if (module_name) { + trace.object_filename = module_name; + } + // We also look after the name of the symbol, equal or before this + // address. This is found by walking the symtab. We should get the + // symbol corresponding to the function (mangled) containing the + // address. If the code corresponding to the address was inlined, + // this is the name of the out-most inliner function. + const char* sym_name = dwfl_module_addrname(mod, trace_addr); + if (sym_name) { + trace.object_function = demangle(sym_name); + } + } + + // now let's get serious, and find out the source location (file and + // line number) of the address. + + // This function will look in .debug_aranges for the address and map it + // to the location of the compilation unit DIE in .debug_info and + // return it. + Dwarf_Addr mod_bias = 0; + Dwarf_Die* cudie = dwfl_module_addrdie(mod, trace_addr, &mod_bias); + +#if 1 + if (!cudie) { + // Sadly clang does not generate the section .debug_aranges, thus + // dwfl_module_addrdie will fail early. Clang doesn't either set + // the lowpc/highpc/range info for every compilation unit. + // + // So in order to save the world: + // for every compilation unit, we will iterate over every single + // DIEs. Normally functions should have a lowpc/highpc/range, which + // we will use to infer the compilation unit. + + // note that this is probably badly inefficient. + while ((cudie = dwfl_module_nextcu(mod, cudie, &mod_bias))) { + Dwarf_Die die_mem; + Dwarf_Die* fundie = find_fundie_by_pc(cudie, + trace_addr - mod_bias, &die_mem); + if (fundie) { + break; + } + } + } +#endif + +//#define BACKWARD_I_DO_NOT_RECOMMEND_TO_ENABLE_THIS_HORRIBLE_PIECE_OF_CODE +#ifdef BACKWARD_I_DO_NOT_RECOMMEND_TO_ENABLE_THIS_HORRIBLE_PIECE_OF_CODE + if (!cudie) { + // If it's still not enough, lets dive deeper in the shit, and try + // to save the world again: for every compilation unit, we will + // load the corresponding .debug_line section, and see if we can + // find our address in it. + + Dwarf_Addr cfi_bias; + Dwarf_CFI* cfi_cache = dwfl_module_eh_cfi(mod, &cfi_bias); + + Dwarf_Addr bias; + while ((cudie = dwfl_module_nextcu(mod, cudie, &bias))) { + if (dwarf_getsrc_die(cudie, trace_addr - bias)) { + + // ...but if we get a match, it might be a false positive + // because our (address - bias) might as well be valid in a + // different compilation unit. So we throw our last card on + // the table and lookup for the address into the .eh_frame + // section. + + handle frame; + dwarf_cfi_addrframe(cfi_cache, trace_addr - cfi_bias, &frame); + if (frame) { + break; + } + } + } + } +#endif + + if (!cudie) { + return trace; // this time we lost the game :/ + } + + // Now that we have a compilation unit DIE, this function will be able + // to load the corresponding section in .debug_line (if not already + // loaded) and hopefully find the source location mapped to our + // address. + Dwarf_Line* srcloc = dwarf_getsrc_die(cudie, trace_addr - mod_bias); + + if (srcloc) { + const char* srcfile = dwarf_linesrc(srcloc, 0, 0); + if (srcfile) { + trace.source.filename = srcfile; + } + int line = 0, col = 0; + dwarf_lineno(srcloc, &line); + dwarf_linecol(srcloc, &col); + trace.source.line = line; + trace.source.col = col; + } + + deep_first_search_by_pc(cudie, trace_addr - mod_bias, + inliners_search_cb(trace)); + if (trace.source.function.size() == 0) { + // fallback. + trace.source.function = trace.object_function; + } + + return trace; + } + +private: + typedef details::handle > + dwfl_handle_t; + details::handle > + _dwfl_cb; + dwfl_handle_t _dwfl_handle; + bool _dwfl_handle_initialized; + + // defined here because in C++98, template function cannot take locally + // defined types... grrr. + struct inliners_search_cb { + void operator()(Dwarf_Die* die) { + switch (dwarf_tag(die)) { + const char* name; + case DW_TAG_subprogram: + if ((name = dwarf_diename(die))) { + trace.source.function = name; + } + break; + + case DW_TAG_inlined_subroutine: + ResolvedTrace::SourceLoc sloc; + Dwarf_Attribute attr_mem; + + if ((name = dwarf_diename(die))) { + sloc.function = name; + } + if ((name = die_call_file(die))) { + sloc.filename = name; + } + + Dwarf_Word line = 0, col = 0; + dwarf_formudata(dwarf_attr(die, DW_AT_call_line, + &attr_mem), &line); + dwarf_formudata(dwarf_attr(die, DW_AT_call_column, + &attr_mem), &col); + sloc.line = (unsigned)line; + sloc.col = (unsigned)col; + + trace.inliners.push_back(sloc); + break; + }; + } + ResolvedTrace& trace; + inliners_search_cb(ResolvedTrace& t): trace(t) {} + }; + + + static bool die_has_pc(Dwarf_Die* die, Dwarf_Addr pc) { + Dwarf_Addr low, high; + + // continuous range + if (dwarf_hasattr(die, DW_AT_low_pc) && + dwarf_hasattr(die, DW_AT_high_pc)) { + if (dwarf_lowpc(die, &low) != 0) { + return false; + } + if (dwarf_highpc(die, &high) != 0) { + Dwarf_Attribute attr_mem; + Dwarf_Attribute* attr = dwarf_attr(die, DW_AT_high_pc, &attr_mem); + Dwarf_Word value; + if (dwarf_formudata(attr, &value) != 0) { + return false; + } + high = low + value; + } + return pc >= low && pc < high; + } + + // non-continuous range. + Dwarf_Addr base; + ptrdiff_t offset = 0; + while ((offset = dwarf_ranges(die, offset, &base, &low, &high)) > 0) { + if (pc >= low && pc < high) { + return true; + } + } + return false; + } + + static Dwarf_Die* find_fundie_by_pc(Dwarf_Die* parent_die, Dwarf_Addr pc, + Dwarf_Die* result) { + if (dwarf_child(parent_die, result) != 0) { + return 0; + } + + Dwarf_Die* die = result; + do { + switch (dwarf_tag(die)) { + case DW_TAG_subprogram: + case DW_TAG_inlined_subroutine: + if (die_has_pc(die, pc)) { + return result; + } + }; + bool declaration = false; + Dwarf_Attribute attr_mem; + dwarf_formflag(dwarf_attr(die, DW_AT_declaration, + &attr_mem), &declaration); + if (!declaration) { + // let's be curious and look deeper in the tree, + // function are not necessarily at the first level, but + // might be nested inside a namespace, structure etc. + Dwarf_Die die_mem; + Dwarf_Die* indie = find_fundie_by_pc(die, pc, &die_mem); + if (indie) { + *result = die_mem; + return result; + } + } + } while (dwarf_siblingof(die, result) == 0); + return 0; + } + + template + static bool deep_first_search_by_pc(Dwarf_Die* parent_die, + Dwarf_Addr pc, CB cb) { + Dwarf_Die die_mem; + if (dwarf_child(parent_die, &die_mem) != 0) { + return false; + } + + bool branch_has_pc = false; + Dwarf_Die* die = &die_mem; + do { + bool declaration = false; + Dwarf_Attribute attr_mem; + dwarf_formflag(dwarf_attr(die, DW_AT_declaration, &attr_mem), &declaration); + if (!declaration) { + // let's be curious and look deeper in the tree, function are + // not necessarily at the first level, but might be nested + // inside a namespace, structure, a function, an inlined + // function etc. + branch_has_pc = deep_first_search_by_pc(die, pc, cb); + } + if (!branch_has_pc) { + branch_has_pc = die_has_pc(die, pc); + } + if (branch_has_pc) { + cb(die); + } + } while (dwarf_siblingof(die, &die_mem) == 0); + return branch_has_pc; + } + + static const char* die_call_file(Dwarf_Die *die) { + Dwarf_Attribute attr_mem; + Dwarf_Sword file_idx = 0; + + dwarf_formsdata(dwarf_attr(die, DW_AT_call_file, &attr_mem), + &file_idx); + + if (file_idx == 0) { + return 0; + } + + Dwarf_Die die_mem; + Dwarf_Die* cudie = dwarf_diecu(die, &die_mem, 0, 0); + if (!cudie) { + return 0; + } + + Dwarf_Files* files = 0; + size_t nfiles; + dwarf_getsrcfiles(cudie, &files, &nfiles); + if (!files) { + return 0; + } + + return dwarf_filesrc(files, file_idx, 0, 0); + } + +}; +#endif // BACKWARD_HAS_DW == 1 + +#if BACKWARD_HAS_DWARF == 1 + +template <> +class TraceResolverLinuxImpl: + public TraceResolverImplBase { + static std::string read_symlink(std::string const & symlink_path) { + std::string path; + path.resize(100); + + while(true) { + ssize_t len = ::readlink(symlink_path.c_str(), + &*path.begin(), path.size()); + if(len < 0) { + return ""; + } + if ((size_t)len == path.size()) { + path.resize(path.size() * 2); + } + else { + path.resize(len); + break; + } + } + + return path; + } +public: + TraceResolverLinuxImpl(): _dwarf_loaded(false) {} + + template + void load_stacktrace(ST&) {} + + ResolvedTrace resolve(ResolvedTrace trace) { + // trace.addr is a virtual address in memory pointing to some code. + // Let's try to find from which loaded object it comes from. + // The loaded object can be yourself btw. + + Dl_info symbol_info; + int dladdr_result = 0; +#ifndef __ANDROID__ + link_map *link_map; + // We request the link map so we can get information about offsets + dladdr_result = dladdr1(trace.addr, &symbol_info, + reinterpret_cast(&link_map), RTLD_DL_LINKMAP); +#else + // Android doesn't have dladdr1. Don't use the linker map. + dladdr_result = dladdr(trace.addr, &symbol_info); +#endif + if (!dladdr_result) { + return trace; // dat broken trace... + } + + std::string argv0; + { + std::ifstream ifs("/proc/self/cmdline"); + std::getline(ifs, argv0, '\0'); + } + std::string tmp; + if(symbol_info.dli_fname == argv0) { + tmp = read_symlink("/proc/self/exe"); + symbol_info.dli_fname = tmp.c_str(); + } + + // Now we get in symbol_info: + // .dli_fname: + // pathname of the shared object that contains the address. + // .dli_fbase: + // where the object is loaded in memory. + // .dli_sname: + // the name of the nearest symbol to trace.addr, we expect a + // function name. + // .dli_saddr: + // the exact address corresponding to .dli_sname. + // + // And in link_map: + // .l_addr: + // difference between the address in the ELF file and the address + // in memory + // l_name: + // absolute pathname where the object was found + + if (symbol_info.dli_sname) { + trace.object_function = demangle(symbol_info.dli_sname); + } + + if (!symbol_info.dli_fname) { + return trace; + } + + trace.object_filename = symbol_info.dli_fname; + dwarf_fileobject& fobj = load_object_with_dwarf(symbol_info.dli_fname); + if (!fobj.dwarf_handle) { + return trace; // sad, we couldn't load the object :( + } + +#ifndef __ANDROID__ + // Convert the address to a module relative one by looking at + // the module's loading address in the link map + Dwarf_Addr address = reinterpret_cast(trace.addr) - + reinterpret_cast(link_map->l_addr); +#else + Dwarf_Addr address = reinterpret_cast(trace.addr); +#endif + + if (trace.object_function.empty()) { + symbol_cache_t::iterator it = + fobj.symbol_cache.lower_bound(address); + + if (it != fobj.symbol_cache.end()) { + if (it->first != address) { + if (it != fobj.symbol_cache.begin()) { + --it; + } + } + trace.object_function = demangle(it->second.c_str()); + } + } + + // Get the Compilation Unit DIE for the address + Dwarf_Die die = find_die(fobj, address); + + if (!die) { + return trace; // this time we lost the game :/ + } + + // libdwarf doesn't give us direct access to its objects, it always + // allocates a copy for the caller. We keep that copy alive in a cache + // and we deallocate it later when it's no longer required. + die_cache_entry& die_object = get_die_cache(fobj, die); + if (die_object.isEmpty()) + return trace; // We have no line section for this DIE + + die_linemap_t::iterator it = + die_object.line_section.lower_bound(address); + + if (it != die_object.line_section.end()) { + if (it->first != address) { + if (it == die_object.line_section.begin()) { + // If we are on the first item of the line section + // but the address does not match it means that + // the address is below the range of the DIE. Give up. + return trace; + } else { + --it; + } + } + } else { + return trace; // We didn't find the address. + } + + // Get the Dwarf_Line that the address points to and call libdwarf + // to get source file, line and column info. + Dwarf_Line line = die_object.line_buffer[it->second]; + Dwarf_Error error = DW_DLE_NE; + + char* filename; + if (dwarf_linesrc(line, &filename, &error) + == DW_DLV_OK) { + trace.source.filename = std::string(filename); + dwarf_dealloc(fobj.dwarf_handle.get(), filename, DW_DLA_STRING); + } + + Dwarf_Unsigned number = 0; + if (dwarf_lineno(line, &number, &error) == DW_DLV_OK) { + trace.source.line = number; + } else { + trace.source.line = 0; + } + + if (dwarf_lineoff_b(line, &number, &error) == DW_DLV_OK) { + trace.source.col = number; + } else { + trace.source.col = 0; + } + + std::vector namespace_stack; + deep_first_search_by_pc(fobj, die, address, namespace_stack, + inliners_search_cb(trace, fobj, die)); + + dwarf_dealloc(fobj.dwarf_handle.get(), die, DW_DLA_DIE); + + return trace; + } + +public: + static int close_dwarf(Dwarf_Debug dwarf) { + return dwarf_finish(dwarf, NULL); + } + +private: + bool _dwarf_loaded; + + typedef details::handle + > dwarf_file_t; + + typedef details::handle + > dwarf_elf_t; + + typedef details::handle + > dwarf_handle_t; + + typedef std::map die_linemap_t; + + typedef std::map die_specmap_t; + + struct die_cache_entry { + die_specmap_t spec_section; + die_linemap_t line_section; + Dwarf_Line* line_buffer; + Dwarf_Signed line_count; + Dwarf_Line_Context line_context; + + inline bool isEmpty() { + return line_buffer == NULL || + line_count == 0 || + line_context == NULL || + line_section.empty(); + } + + die_cache_entry() : + line_buffer(0), line_count(0), line_context(0) {} + + ~die_cache_entry() + { + if (line_context) { + dwarf_srclines_dealloc_b(line_context); + } + } + }; + + typedef std::map die_cache_t; + + typedef std::map symbol_cache_t; + + struct dwarf_fileobject { + dwarf_file_t file_handle; + dwarf_elf_t elf_handle; + dwarf_handle_t dwarf_handle; + symbol_cache_t symbol_cache; + + // Die cache + die_cache_t die_cache; + die_cache_entry* current_cu; + }; + + typedef details::hashtable::type + fobj_dwarf_map_t; + fobj_dwarf_map_t _fobj_dwarf_map; + + static bool cstrings_eq(const char* a, const char* b) { + if (!a || !b) { + return false; + } + return strcmp(a, b) == 0; + } + + dwarf_fileobject& load_object_with_dwarf( + const std::string& filename_object) { + + if (!_dwarf_loaded) { + // Set the ELF library operating version + // If that fails there's nothing we can do + _dwarf_loaded = elf_version(EV_CURRENT) != EV_NONE; + } + + fobj_dwarf_map_t::iterator it = + _fobj_dwarf_map.find(filename_object); + if (it != _fobj_dwarf_map.end()) { + return it->second; + } + + // this new object is empty for now + dwarf_fileobject& r = _fobj_dwarf_map[filename_object]; + + dwarf_file_t file_handle; + file_handle.reset(open(filename_object.c_str(), O_RDONLY)); + if (file_handle < 0) { + return r; + } + + // Try to get an ELF handle. We need to read the ELF sections + // because we want to see if there is a .gnu_debuglink section + // that points to a split debug file + dwarf_elf_t elf_handle; + elf_handle.reset(elf_begin(file_handle.get(), ELF_C_READ, NULL)); + if (!elf_handle) { + return r; + } + + const char* e_ident = elf_getident(elf_handle.get(), 0); + if (!e_ident) { + return r; + } + + // Get the number of sections + // We use the new APIs as elf_getshnum is deprecated + size_t shdrnum = 0; + if (elf_getshdrnum(elf_handle.get(), &shdrnum) == -1) { + return r; + } + + // Get the index to the string section + size_t shdrstrndx = 0; + if (elf_getshdrstrndx (elf_handle.get(), &shdrstrndx) == -1) { + return r; + } + + std::string debuglink; + // Iterate through the ELF sections to try to get a gnu_debuglink + // note and also to cache the symbol table. + // We go the preprocessor way to avoid having to create templated + // classes or using gelf (which might throw a compiler error if 64 bit + // is not supported +#define ELF_GET_DATA(ARCH) \ + Elf_Scn *elf_section = 0; \ + Elf_Data *elf_data = 0; \ + Elf##ARCH##_Shdr* section_header = 0; \ + Elf_Scn *symbol_section = 0; \ + size_t symbol_count = 0; \ + size_t symbol_strings = 0; \ + Elf##ARCH##_Sym *symbol = 0; \ + const char* section_name = 0; \ + \ + while ((elf_section = elf_nextscn(elf_handle.get(), elf_section)) \ + != NULL) { \ + section_header = elf##ARCH##_getshdr(elf_section); \ + if (section_header == NULL) { \ + return r; \ + } \ + \ + if ((section_name = elf_strptr( \ + elf_handle.get(), shdrstrndx, \ + section_header->sh_name)) == NULL) { \ + return r; \ + } \ + \ + if (cstrings_eq(section_name, ".gnu_debuglink")) { \ + elf_data = elf_getdata(elf_section, NULL); \ + if (elf_data && elf_data->d_size > 0) { \ + debuglink = std::string( \ + reinterpret_cast(elf_data->d_buf)); \ + } \ + } \ + \ + switch(section_header->sh_type) { \ + case SHT_SYMTAB: \ + symbol_section = elf_section; \ + symbol_count = section_header->sh_size / \ + section_header->sh_entsize; \ + symbol_strings = section_header->sh_link; \ + break; \ + \ + /* We use .dynsyms as a last resort, we prefer .symtab */ \ + case SHT_DYNSYM: \ + if (!symbol_section) { \ + symbol_section = elf_section; \ + symbol_count = section_header->sh_size / \ + section_header->sh_entsize; \ + symbol_strings = section_header->sh_link; \ + } \ + break; \ + } \ + } \ + \ + if (symbol_section && symbol_count && symbol_strings) { \ + elf_data = elf_getdata(symbol_section, NULL); \ + symbol = reinterpret_cast(elf_data->d_buf); \ + for (size_t i = 0; i < symbol_count; ++i) { \ + int type = ELF##ARCH##_ST_TYPE(symbol->st_info); \ + if (type == STT_FUNC && symbol->st_value > 0) { \ + r.symbol_cache[symbol->st_value] = std::string( \ + elf_strptr(elf_handle.get(), \ + symbol_strings, symbol->st_name)); \ + } \ + ++symbol; \ + } \ + } \ + + + if (e_ident[EI_CLASS] == ELFCLASS32) { + ELF_GET_DATA(32) + } else if (e_ident[EI_CLASS] == ELFCLASS64) { + // libelf might have been built without 64 bit support +#if __LIBELF64 + ELF_GET_DATA(64) +#endif + } + + if (!debuglink.empty()) { + // We have a debuglink section! Open an elf instance on that + // file instead. If we can't open the file, then return + // the elf handle we had already opened. + dwarf_file_t debuglink_file; + debuglink_file.reset(open(debuglink.c_str(), O_RDONLY)); + if (debuglink_file.get() > 0) { + dwarf_elf_t debuglink_elf; + debuglink_elf.reset( + elf_begin(debuglink_file.get(),ELF_C_READ, NULL) + ); + + // If we have a valid elf handle, return the new elf handle + // and file handle and discard the original ones + if (debuglink_elf) { + elf_handle = move(debuglink_elf); + file_handle = move(debuglink_file); + } + } + } + + // Ok, we have a valid ELF handle, let's try to get debug symbols + Dwarf_Debug dwarf_debug; + Dwarf_Error error = DW_DLE_NE; + dwarf_handle_t dwarf_handle; + + int dwarf_result = dwarf_elf_init(elf_handle.get(), + DW_DLC_READ, NULL, NULL, &dwarf_debug, &error); + + // We don't do any special handling for DW_DLV_NO_ENTRY specially. + // If we get an error, or the file doesn't have debug information + // we just return. + if (dwarf_result != DW_DLV_OK) { + return r; + } + + dwarf_handle.reset(dwarf_debug); + + r.file_handle = move(file_handle); + r.elf_handle = move(elf_handle); + r.dwarf_handle = move(dwarf_handle); + + return r; + } + + die_cache_entry& get_die_cache(dwarf_fileobject& fobj, Dwarf_Die die) + { + Dwarf_Error error = DW_DLE_NE; + + // Get the die offset, we use it as the cache key + Dwarf_Off die_offset; + if (dwarf_dieoffset(die, &die_offset, &error) != DW_DLV_OK) { + die_offset = 0; + } + + die_cache_t::iterator it = fobj.die_cache.find(die_offset); + + if (it != fobj.die_cache.end()) { + fobj.current_cu = &it->second; + return it->second; + } + + die_cache_entry& de = fobj.die_cache[die_offset]; + fobj.current_cu = &de; + + Dwarf_Addr line_addr; + Dwarf_Small table_count; + + // The addresses in the line section are not fully sorted (they might + // be sorted by block of code belonging to the same file), which makes + // it necessary to do so before searching is possible. + // + // As libdwarf allocates a copy of everything, let's get the contents + // of the line section and keep it around. We also create a map of + // program counter to line table indices so we can search by address + // and get the line buffer index. + // + // To make things more difficult, the same address can span more than + // one line, so we need to keep the index pointing to the first line + // by using insert instead of the map's [ operator. + + // Get the line context for the DIE + if (dwarf_srclines_b(die, 0, &table_count, &de.line_context, &error) + == DW_DLV_OK) { + // Get the source lines for this line context, to be deallocated + // later + if (dwarf_srclines_from_linecontext( + de.line_context, &de.line_buffer, &de.line_count, &error) + == DW_DLV_OK) { + + // Add all the addresses to our map + for (int i = 0; i < de.line_count; i++) { + if (dwarf_lineaddr(de.line_buffer[i], &line_addr, &error) + != DW_DLV_OK) { + line_addr = 0; + } + de.line_section.insert( + std::pair(line_addr, i)); + } + } + } + + // For each CU, cache the function DIEs that contain the + // DW_AT_specification attribute. When building with -g3 the function + // DIEs are separated in declaration and specification, with the + // declaration containing only the name and parameters and the + // specification the low/high pc and other compiler attributes. + // + // We cache those specifications so we don't skip over the declarations, + // because they have no pc, and we can do namespace resolution for + // DWARF function names. + Dwarf_Debug dwarf = fobj.dwarf_handle.get(); + Dwarf_Die current_die = 0; + if (dwarf_child(die, ¤t_die, &error) == DW_DLV_OK) { + for(;;) { + Dwarf_Die sibling_die = 0; + + Dwarf_Half tag_value; + dwarf_tag(current_die, &tag_value, &error); + + if (tag_value == DW_TAG_subprogram || + tag_value == DW_TAG_inlined_subroutine) { + + Dwarf_Bool has_attr = 0; + if (dwarf_hasattr(current_die, DW_AT_specification, + &has_attr, &error) == DW_DLV_OK) { + if (has_attr) { + Dwarf_Attribute attr_mem; + if (dwarf_attr(current_die, DW_AT_specification, + &attr_mem, &error) == DW_DLV_OK) { + Dwarf_Off spec_offset = 0; + if (dwarf_formref(attr_mem, + &spec_offset, &error) == DW_DLV_OK) { + Dwarf_Off spec_die_offset; + if (dwarf_dieoffset(current_die, + &spec_die_offset, &error) + == DW_DLV_OK) { + de.spec_section[spec_offset] = + spec_die_offset; + } + } + } + dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR); + } + } + } + + int result = dwarf_siblingof( + dwarf, current_die, &sibling_die, &error); + if (result == DW_DLV_ERROR) { + break; + } else if (result == DW_DLV_NO_ENTRY) { + break; + } + + if (current_die != die) { + dwarf_dealloc(dwarf, current_die, DW_DLA_DIE); + current_die = 0; + } + + current_die = sibling_die; + } + } + return de; + } + + static Dwarf_Die get_referenced_die( + Dwarf_Debug dwarf, Dwarf_Die die, Dwarf_Half attr, bool global) { + Dwarf_Error error = DW_DLE_NE; + Dwarf_Attribute attr_mem; + + Dwarf_Die found_die = NULL; + if (dwarf_attr(die, attr, &attr_mem, &error) == DW_DLV_OK) { + Dwarf_Off offset; + int result = 0; + if (global) { + result = dwarf_global_formref(attr_mem, &offset, &error); + } else { + result = dwarf_formref(attr_mem, &offset, &error); + } + + if (result == DW_DLV_OK) { + if (dwarf_offdie(dwarf, offset, &found_die, &error) + != DW_DLV_OK) { + found_die = NULL; + } + } + dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR); + } + return found_die; + } + + static std::string get_referenced_die_name( + Dwarf_Debug dwarf, Dwarf_Die die, Dwarf_Half attr, bool global) { + Dwarf_Error error = DW_DLE_NE; + std::string value; + + Dwarf_Die found_die = get_referenced_die(dwarf, die, attr, global); + + if (found_die) { + char *name; + if (dwarf_diename(found_die, &name, &error) == DW_DLV_OK) { + if (name) { + value = std::string(name); + } + dwarf_dealloc(dwarf, name, DW_DLA_STRING); + } + dwarf_dealloc(dwarf, found_die, DW_DLA_DIE); + } + + return value; + } + + // Returns a spec DIE linked to the passed one. The caller should + // deallocate the DIE + static Dwarf_Die get_spec_die(dwarf_fileobject& fobj, Dwarf_Die die) { + Dwarf_Debug dwarf = fobj.dwarf_handle.get(); + Dwarf_Error error = DW_DLE_NE; + Dwarf_Off die_offset; + if (fobj.current_cu && dwarf_die_CU_offset(die, &die_offset, &error) + == DW_DLV_OK) { + die_specmap_t::iterator it = + fobj.current_cu->spec_section.find(die_offset); + + // If we have a DIE that completes the current one, check if + // that one has the pc we are looking for + if (it != fobj.current_cu->spec_section.end()) { + Dwarf_Die spec_die = 0; + if (dwarf_offdie(dwarf, it->second, &spec_die, &error) + == DW_DLV_OK) { + return spec_die; + } + } + } + + // Maybe we have an abstract origin DIE with the function information? + return get_referenced_die( + fobj.dwarf_handle.get(), die, DW_AT_abstract_origin, true); + + } + + static bool die_has_pc(dwarf_fileobject& fobj, Dwarf_Die die, Dwarf_Addr pc) + { + Dwarf_Addr low_pc = 0, high_pc = 0; + Dwarf_Half high_pc_form = 0; + Dwarf_Form_Class return_class; + Dwarf_Error error = DW_DLE_NE; + Dwarf_Debug dwarf = fobj.dwarf_handle.get(); + bool has_lowpc = false; + bool has_highpc = false; + bool has_ranges = false; + + if (dwarf_lowpc(die, &low_pc, &error) == DW_DLV_OK) { + // If we have a low_pc check if there is a high pc. + // If we don't have a high pc this might mean we have a base + // address for the ranges list or just an address. + has_lowpc = true; + + if (dwarf_highpc_b( + die, &high_pc, &high_pc_form, &return_class, &error) + == DW_DLV_OK) { + // We do have a high pc. In DWARF 4+ this is an offset from the + // low pc, but in earlier versions it's an absolute address. + + has_highpc = true; + // In DWARF 2/3 this would be a DW_FORM_CLASS_ADDRESS + if (return_class == DW_FORM_CLASS_CONSTANT) { + high_pc = low_pc + high_pc; + } + + // We have low and high pc, check if our address + // is in that range + return pc >= low_pc && pc < high_pc; + } + } else { + // Reset the low_pc, in case dwarf_lowpc failing set it to some + // undefined value. + low_pc = 0; + } + + // Check if DW_AT_ranges is present and search for the PC in the + // returned ranges list. We always add the low_pc, as it not set it will + // be 0, in case we had a DW_AT_low_pc and DW_AT_ranges pair + bool result = false; + + Dwarf_Attribute attr; + if (dwarf_attr(die, DW_AT_ranges, &attr, &error) == DW_DLV_OK) { + + Dwarf_Off offset; + if (dwarf_global_formref(attr, &offset, &error) == DW_DLV_OK) { + Dwarf_Ranges *ranges; + Dwarf_Signed ranges_count = 0; + Dwarf_Unsigned byte_count = 0; + + if (dwarf_get_ranges_a(dwarf, offset, die, &ranges, + &ranges_count, &byte_count, &error) == DW_DLV_OK) { + has_ranges = ranges_count != 0; + for (int i = 0; i < ranges_count; i++) { + if (ranges[i].dwr_addr1 != 0 && + pc >= ranges[i].dwr_addr1 + low_pc && + pc < ranges[i].dwr_addr2 + low_pc) { + result = true; + break; + } + } + dwarf_ranges_dealloc(dwarf, ranges, ranges_count); + } + } + } + + // Last attempt. We might have a single address set as low_pc. + if (!result && low_pc != 0 && pc == low_pc) { + result = true; + } + + // If we don't have lowpc, highpc and ranges maybe this DIE is a + // declaration that relies on a DW_AT_specification DIE that happens + // later. Use the specification cache we filled when we loaded this CU. + if (!result && (!has_lowpc && !has_highpc && !has_ranges)) { + Dwarf_Die spec_die = get_spec_die(fobj, die); + if (spec_die) { + result = die_has_pc(fobj, spec_die, pc); + dwarf_dealloc(dwarf, spec_die, DW_DLA_DIE); + } + } + + return result; + } + + static void get_type(Dwarf_Debug dwarf, Dwarf_Die die, std::string& type) { + Dwarf_Error error = DW_DLE_NE; + + Dwarf_Die child = 0; + if (dwarf_child(die, &child, &error) == DW_DLV_OK) { + get_type(dwarf, child, type); + } + + if (child) { + type.insert(0, "::"); + dwarf_dealloc(dwarf, child, DW_DLA_DIE); + } + + char *name; + if (dwarf_diename(die, &name, &error) == DW_DLV_OK) { + type.insert(0, std::string(name)); + dwarf_dealloc(dwarf, name, DW_DLA_STRING); + } else { + type.insert(0,""); + } + } + + static std::string get_type_by_signature(Dwarf_Debug dwarf, Dwarf_Die die) { + Dwarf_Error error = DW_DLE_NE; + + Dwarf_Sig8 signature; + Dwarf_Bool has_attr = 0; + if (dwarf_hasattr(die, DW_AT_signature, + &has_attr, &error) == DW_DLV_OK) { + if (has_attr) { + Dwarf_Attribute attr_mem; + if (dwarf_attr(die, DW_AT_signature, + &attr_mem, &error) == DW_DLV_OK) { + if (dwarf_formsig8(attr_mem, &signature, &error) + != DW_DLV_OK) { + return std::string(""); + } + } + dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR); + } + } + + Dwarf_Unsigned next_cu_header; + Dwarf_Sig8 tu_signature; + std::string result; + bool found = false; + + while (dwarf_next_cu_header_d(dwarf, 0, 0, 0, 0, 0, 0, 0, &tu_signature, + 0, &next_cu_header, 0, &error) == DW_DLV_OK) { + + if (strncmp(signature.signature, tu_signature.signature, 8) == 0) { + Dwarf_Die type_cu_die = 0; + if (dwarf_siblingof_b(dwarf, 0, 0, &type_cu_die, &error) + == DW_DLV_OK) { + Dwarf_Die child_die = 0; + if (dwarf_child(type_cu_die, &child_die, &error) + == DW_DLV_OK) { + get_type(dwarf, child_die, result); + found = !result.empty(); + dwarf_dealloc(dwarf, child_die, DW_DLA_DIE); + } + dwarf_dealloc(dwarf, type_cu_die, DW_DLA_DIE); + } + } + } + + if (found) { + while (dwarf_next_cu_header_d(dwarf, 0, 0, 0, 0, 0, 0, 0, 0, 0, + &next_cu_header, 0, &error) == DW_DLV_OK) { + // Reset the cu header state. Unfortunately, libdwarf's + // next_cu_header API keeps its own iterator per Dwarf_Debug + // that can't be reset. We need to keep fetching elements until + // the end. + } + } else { + // If we couldn't resolve the type just print out the signature + std::ostringstream string_stream; + string_stream << "<0x" << + std::hex << std::setfill('0'); + for (int i = 0; i < 8; ++i) { + string_stream << std::setw(2) << std::hex + << (int)(unsigned char)(signature.signature[i]); + } + string_stream << ">"; + result = string_stream.str(); + } + return result; + } + + struct type_context_t { + bool is_const; + bool is_typedef; + bool has_type; + bool has_name; + std::string text; + + type_context_t() : + is_const(false), is_typedef(false), + has_type(false), has_name(false) {} + }; + + // Types are resolved from right to left: we get the variable name first + // and then all specifiers (like const or pointer) in a chain of DW_AT_type + // DIEs. Call this function recursively until we get a complete type + // string. + static void set_parameter_string( + dwarf_fileobject& fobj, Dwarf_Die die, type_context_t &context) { + char *name; + Dwarf_Error error = DW_DLE_NE; + + // typedefs contain also the base type, so we skip it and only + // print the typedef name + if (!context.is_typedef) { + if (dwarf_diename(die, &name, &error) == DW_DLV_OK) { + if (!context.text.empty()) { + context.text.insert(0, " "); + } + context.text.insert(0, std::string(name)); + dwarf_dealloc(fobj.dwarf_handle.get(), name, DW_DLA_STRING); + } + } else { + context.is_typedef = false; + context.has_type = true; + if (context.is_const) { + context.text.insert(0, "const "); + context.is_const = false; + } + } + + bool next_type_is_const = false; + bool is_keyword = true; + + Dwarf_Half tag = 0; + Dwarf_Bool has_attr = 0; + if (dwarf_tag(die, &tag, &error) == DW_DLV_OK) { + switch(tag) { + case DW_TAG_structure_type: + case DW_TAG_union_type: + case DW_TAG_class_type: + case DW_TAG_enumeration_type: + context.has_type = true; + if (dwarf_hasattr(die, DW_AT_signature, + &has_attr, &error) == DW_DLV_OK) { + // If we have a signature it means the type is defined + // in .debug_types, so we need to load the DIE pointed + // at by the signature and resolve it + if (has_attr) { + std::string type = + get_type_by_signature(fobj.dwarf_handle.get(), die); + if (context.is_const) + type.insert(0, "const "); + + if (!context.text.empty()) + context.text.insert(0, " "); + context.text.insert(0, type); + } + + // Treat enums like typedefs, and skip printing its + // base type + context.is_typedef = (tag == DW_TAG_enumeration_type); + } + break; + case DW_TAG_const_type: + next_type_is_const = true; + break; + case DW_TAG_pointer_type: + context.text.insert(0, "*"); + break; + case DW_TAG_reference_type: + context.text.insert(0, "&"); + break; + case DW_TAG_restrict_type: + context.text.insert(0, "restrict "); + break; + case DW_TAG_rvalue_reference_type: + context.text.insert(0, "&&"); + break; + case DW_TAG_volatile_type: + context.text.insert(0, "volatile "); + break; + case DW_TAG_typedef: + // Propagate the const-ness to the next type + // as typedefs are linked to its base type + next_type_is_const = context.is_const; + context.is_typedef = true; + context.has_type = true; + break; + case DW_TAG_base_type: + context.has_type = true; + break; + case DW_TAG_formal_parameter: + context.has_name = true; + break; + default: + is_keyword = false; + break; + } + } + + if (!is_keyword && context.is_const) { + context.text.insert(0, "const "); + } + + context.is_const = next_type_is_const; + + Dwarf_Die ref = get_referenced_die( + fobj.dwarf_handle.get(), die, DW_AT_type, true); + if (ref) { + set_parameter_string(fobj, ref, context); + dwarf_dealloc(fobj.dwarf_handle.get(), ref, DW_DLA_DIE); + } + + if (!context.has_type && context.has_name) { + context.text.insert(0, "void "); + context.has_type = true; + } + } + + // Resolve the function return type and parameters + static void set_function_parameters(std::string& function_name, + std::vector& ns, + dwarf_fileobject& fobj, Dwarf_Die die) { + Dwarf_Debug dwarf = fobj.dwarf_handle.get(); + Dwarf_Error error = DW_DLE_NE; + Dwarf_Die current_die = 0; + std::string parameters; + bool has_spec = true; + // Check if we have a spec DIE. If we do we use it as it contains + // more information, like parameter names. + Dwarf_Die spec_die = get_spec_die(fobj, die); + if (!spec_die) { + has_spec = false; + spec_die = die; + } + + std::vector::const_iterator it = ns.begin(); + std::string ns_name; + for (it = ns.begin(); it < ns.end(); ++it) { + ns_name.append(*it).append("::"); + } + + if (!ns_name.empty()) { + function_name.insert(0, ns_name); + } + + // See if we have a function return type. It can be either on the + // current die or in its spec one (usually true for inlined functions) + std::string return_type = + get_referenced_die_name(dwarf, die, DW_AT_type, true); + if (return_type.empty()) { + return_type = + get_referenced_die_name(dwarf, spec_die, DW_AT_type, true); + } + if (!return_type.empty()) { + return_type.append(" "); + function_name.insert(0, return_type); + } + + if (dwarf_child(spec_die, ¤t_die, &error) == DW_DLV_OK) { + for(;;) { + Dwarf_Die sibling_die = 0; + + Dwarf_Half tag_value; + dwarf_tag(current_die, &tag_value, &error); + + if (tag_value == DW_TAG_formal_parameter) { + // Ignore artificial (ie, compiler generated) parameters + bool is_artificial = false; + Dwarf_Attribute attr_mem; + if (dwarf_attr( + current_die, DW_AT_artificial, &attr_mem, &error) + == DW_DLV_OK) { + Dwarf_Bool flag = 0; + if (dwarf_formflag(attr_mem, &flag, &error) + == DW_DLV_OK) { + is_artificial = flag != 0; + } + dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR); + } + + if (!is_artificial) { + type_context_t context; + set_parameter_string(fobj, current_die, context); + + if (parameters.empty()) { + parameters.append("("); + } else { + parameters.append(", "); + } + parameters.append(context.text); + } + } + + int result = dwarf_siblingof( + dwarf, current_die, &sibling_die, &error); + if (result == DW_DLV_ERROR) { + break; + } else if (result == DW_DLV_NO_ENTRY) { + break; + } + + if (current_die != die) { + dwarf_dealloc(dwarf, current_die, DW_DLA_DIE); + current_die = 0; + } + + current_die = sibling_die; + } + } + if (parameters.empty()) + parameters = "("; + parameters.append(")"); + + // If we got a spec DIE we need to deallocate it + if (has_spec) + dwarf_dealloc(dwarf, spec_die, DW_DLA_DIE); + + function_name.append(parameters); + } + + // defined here because in C++98, template function cannot take locally + // defined types... grrr. + struct inliners_search_cb { + void operator()(Dwarf_Die die, std::vector& ns) { + Dwarf_Error error = DW_DLE_NE; + Dwarf_Half tag_value; + Dwarf_Attribute attr_mem; + Dwarf_Debug dwarf = fobj.dwarf_handle.get(); + + dwarf_tag(die, &tag_value, &error); + + switch (tag_value) { + char* name; + case DW_TAG_subprogram: + if (!trace.source.function.empty()) + break; + if (dwarf_diename(die, &name, &error) == DW_DLV_OK) { + trace.source.function = std::string(name); + dwarf_dealloc(dwarf, name, DW_DLA_STRING); + } else { + // We don't have a function name in this DIE. + // Check if there is a referenced non-defining + // declaration. + trace.source.function = get_referenced_die_name( + dwarf, die, DW_AT_abstract_origin, true); + if (trace.source.function.empty()) { + trace.source.function = get_referenced_die_name( + dwarf, die, DW_AT_specification, true); + } + } + + // Append the function parameters, if available + set_function_parameters( + trace.source.function, ns, fobj, die); + + // If the object function name is empty, it's possible that + // there is no dynamic symbol table (maybe the executable + // was stripped or not built with -rdynamic). See if we have + // a DWARF linkage name to use instead. We try both + // linkage_name and MIPS_linkage_name because the MIPS tag + // was the unofficial one until it was adopted in DWARF4. + // Old gcc versions generate MIPS_linkage_name + if (trace.object_function.empty()) { + details::demangler demangler; + + if (dwarf_attr(die, DW_AT_linkage_name, + &attr_mem, &error) != DW_DLV_OK) { + if (dwarf_attr(die, DW_AT_MIPS_linkage_name, + &attr_mem, &error) != DW_DLV_OK) { + break; + } + } + + char* linkage; + if (dwarf_formstring(attr_mem, &linkage, &error) + == DW_DLV_OK) { + trace.object_function = demangler.demangle(linkage); + dwarf_dealloc(dwarf, linkage, DW_DLA_STRING); + } + dwarf_dealloc(dwarf, name, DW_DLA_ATTR); + } + break; + + case DW_TAG_inlined_subroutine: + ResolvedTrace::SourceLoc sloc; + + if (dwarf_diename(die, &name, &error) == DW_DLV_OK) { + sloc.function = std::string(name); + dwarf_dealloc(dwarf, name, DW_DLA_STRING); + } else { + // We don't have a name for this inlined DIE, it could + // be that there is an abstract origin instead. + // Get the DW_AT_abstract_origin value, which is a + // reference to the source DIE and try to get its name + sloc.function = get_referenced_die_name( + dwarf, die, DW_AT_abstract_origin, true); + } + + set_function_parameters(sloc.function, ns, fobj, die); + + std::string file = die_call_file(dwarf, die, cu_die); + if (!file.empty()) + sloc.filename = file; + + Dwarf_Unsigned number = 0; + if (dwarf_attr(die, DW_AT_call_line, &attr_mem, &error) + == DW_DLV_OK) { + if (dwarf_formudata(attr_mem, &number, &error) + == DW_DLV_OK) { + sloc.line = number; + } + dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR); + } + + if (dwarf_attr(die, DW_AT_call_column, &attr_mem, &error) + == DW_DLV_OK) { + if (dwarf_formudata(attr_mem, &number, &error) + == DW_DLV_OK) { + sloc.col = number; + } + dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR); + } + + trace.inliners.push_back(sloc); + break; + }; + } + ResolvedTrace& trace; + dwarf_fileobject& fobj; + Dwarf_Die cu_die; + inliners_search_cb(ResolvedTrace& t, dwarf_fileobject& f, Dwarf_Die c) + : trace(t), fobj(f), cu_die(c) {} + }; + + static Dwarf_Die find_fundie_by_pc(dwarf_fileobject& fobj, + Dwarf_Die parent_die, Dwarf_Addr pc, Dwarf_Die result) { + Dwarf_Die current_die = 0; + Dwarf_Error error = DW_DLE_NE; + Dwarf_Debug dwarf = fobj.dwarf_handle.get(); + + if (dwarf_child(parent_die, ¤t_die, &error) != DW_DLV_OK) { + return NULL; + } + + for(;;) { + Dwarf_Die sibling_die = 0; + Dwarf_Half tag_value; + dwarf_tag(current_die, &tag_value, &error); + + switch (tag_value) { + case DW_TAG_subprogram: + case DW_TAG_inlined_subroutine: + if (die_has_pc(fobj, current_die, pc)) { + return current_die; + } + }; + bool declaration = false; + Dwarf_Attribute attr_mem; + if (dwarf_attr(current_die, DW_AT_declaration, &attr_mem, &error) + == DW_DLV_OK) { + Dwarf_Bool flag = 0; + if (dwarf_formflag(attr_mem, &flag, &error) == DW_DLV_OK) { + declaration = flag != 0; + } + dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR); + } + + if (!declaration) { + // let's be curious and look deeper in the tree, functions are + // not necessarily at the first level, but might be nested + // inside a namespace, structure, a function, an inlined + // function etc. + Dwarf_Die die_mem = 0; + Dwarf_Die indie = find_fundie_by_pc( + fobj, current_die, pc, die_mem); + if (indie) { + result = die_mem; + return result; + } + } + + int res = dwarf_siblingof( + dwarf, current_die, &sibling_die, &error); + if (res == DW_DLV_ERROR) { + return NULL; + } else if (res == DW_DLV_NO_ENTRY) { + break; + } + + if (current_die != parent_die) { + dwarf_dealloc(dwarf, current_die, DW_DLA_DIE); + current_die = 0; + } + + current_die = sibling_die; + } + return NULL; + } + + template + static bool deep_first_search_by_pc(dwarf_fileobject& fobj, + Dwarf_Die parent_die, Dwarf_Addr pc, + std::vector& ns, CB cb) { + Dwarf_Die current_die = 0; + Dwarf_Debug dwarf = fobj.dwarf_handle.get(); + Dwarf_Error error = DW_DLE_NE; + + if (dwarf_child(parent_die, ¤t_die, &error) != DW_DLV_OK) { + return false; + } + + bool branch_has_pc = false; + bool has_namespace = false; + for(;;) { + Dwarf_Die sibling_die = 0; + + Dwarf_Half tag; + if (dwarf_tag(current_die, &tag, &error) == DW_DLV_OK) { + if (tag == DW_TAG_namespace || tag == DW_TAG_class_type) { + char* ns_name = NULL; + if (dwarf_diename(current_die, &ns_name, &error) + == DW_DLV_OK) { + if (ns_name) { + ns.push_back(std::string(ns_name)); + } else { + ns.push_back(""); + } + dwarf_dealloc(dwarf, ns_name, DW_DLA_STRING); + } else { + ns.push_back(""); + } + has_namespace = true; + } + } + + bool declaration = false; + Dwarf_Attribute attr_mem; + if (tag != DW_TAG_class_type && + dwarf_attr(current_die, DW_AT_declaration, &attr_mem, &error) + == DW_DLV_OK) { + Dwarf_Bool flag = 0; + if (dwarf_formflag(attr_mem, &flag, &error) == DW_DLV_OK) { + declaration = flag != 0; + } + dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR); + } + + if (!declaration) { + // let's be curious and look deeper in the tree, function are + // not necessarily at the first level, but might be nested + // inside a namespace, structure, a function, an inlined + // function etc. + branch_has_pc = deep_first_search_by_pc( + fobj, current_die, pc, ns, cb); + } + + if (!branch_has_pc) { + branch_has_pc = die_has_pc(fobj, current_die, pc); + } + + if (branch_has_pc) { + cb(current_die, ns); + } + + int result = dwarf_siblingof( + dwarf, current_die, &sibling_die, &error); + if (result == DW_DLV_ERROR) { + return false; + } else if (result == DW_DLV_NO_ENTRY) { + break; + } + + if (current_die != parent_die) { + dwarf_dealloc(dwarf, current_die, DW_DLA_DIE); + current_die = 0; + } + + if (has_namespace) { + has_namespace = false; + ns.pop_back(); + } + current_die = sibling_die; + } + + if (has_namespace) { + ns.pop_back(); + } + return branch_has_pc; + } + + static std::string die_call_file( + Dwarf_Debug dwarf, Dwarf_Die die, Dwarf_Die cu_die) { + Dwarf_Attribute attr_mem; + Dwarf_Error error = DW_DLE_NE; + Dwarf_Signed file_index; + + std::string file; + + if (dwarf_attr(die, DW_AT_call_file, &attr_mem, &error) == DW_DLV_OK) { + if (dwarf_formsdata(attr_mem, &file_index, &error) != DW_DLV_OK) { + file_index = 0; + } + dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR); + + if (file_index == 0) { + return file; + } + + char **srcfiles = 0; + Dwarf_Signed file_count = 0; + if (dwarf_srcfiles(cu_die, &srcfiles, &file_count, &error) + == DW_DLV_OK) { + if (file_index <= file_count) + file = std::string(srcfiles[file_index - 1]); + + // Deallocate all strings! + for (int i = 0; i < file_count; ++i) { + dwarf_dealloc(dwarf, srcfiles[i], DW_DLA_STRING); + } + dwarf_dealloc(dwarf, srcfiles, DW_DLA_LIST); + } + } + return file; + } + + + Dwarf_Die find_die(dwarf_fileobject& fobj, Dwarf_Addr addr) + { + // Let's get to work! First see if we have a debug_aranges section so + // we can speed up the search + + Dwarf_Debug dwarf = fobj.dwarf_handle.get(); + Dwarf_Error error = DW_DLE_NE; + Dwarf_Arange *aranges; + Dwarf_Signed arange_count; + + Dwarf_Die returnDie; + bool found = false; + if (dwarf_get_aranges( + dwarf, &aranges, &arange_count, &error) != DW_DLV_OK) { + aranges = NULL; + } + + if (aranges) { + // We have aranges. Get the one where our address is. + Dwarf_Arange arange; + if (dwarf_get_arange( + aranges, arange_count, addr, &arange, &error) + == DW_DLV_OK) { + + // We found our address. Get the compilation-unit DIE offset + // represented by the given address range. + Dwarf_Off cu_die_offset; + if (dwarf_get_cu_die_offset(arange, &cu_die_offset, &error) + == DW_DLV_OK) { + // Get the DIE at the offset returned by the aranges search. + // We set is_info to 1 to specify that the offset is from + // the .debug_info section (and not .debug_types) + int dwarf_result = dwarf_offdie_b( + dwarf, cu_die_offset, 1, &returnDie, &error); + + found = dwarf_result == DW_DLV_OK; + } + dwarf_dealloc(dwarf, arange, DW_DLA_ARANGE); + } + } + + if (found) + return returnDie; // The caller is responsible for freeing the die + + // The search for aranges failed. Try to find our address by scanning + // all compilation units. + Dwarf_Unsigned next_cu_header; + Dwarf_Half tag = 0; + returnDie = 0; + + while (!found && dwarf_next_cu_header_d(dwarf, 1, 0, 0, 0, 0, 0, 0, 0, 0, + &next_cu_header, 0, &error) == DW_DLV_OK) { + + if (returnDie) + dwarf_dealloc(dwarf, returnDie, DW_DLA_DIE); + + if (dwarf_siblingof(dwarf, 0, &returnDie, &error) == DW_DLV_OK) { + if ((dwarf_tag(returnDie, &tag, &error) == DW_DLV_OK) + && tag == DW_TAG_compile_unit) { + if (die_has_pc(fobj, returnDie, addr)) { + found = true; + } + } + } + } + + if (found) { + while (dwarf_next_cu_header_d(dwarf, 1, 0, 0, 0, 0, 0, 0, 0, 0, + &next_cu_header, 0, &error) == DW_DLV_OK) { + // Reset the cu header state. Libdwarf's next_cu_header API + // keeps its own iterator per Dwarf_Debug that can't be reset. + // We need to keep fetching elements until the end. + } + } + + if (found) + return returnDie; + + // We couldn't find any compilation units with ranges or a high/low pc. + // Try again by looking at all DIEs in all compilation units. + Dwarf_Die cudie; + while (dwarf_next_cu_header_d(dwarf, 1, 0, 0, 0, 0, 0, 0, 0, 0, + &next_cu_header, 0, &error) == DW_DLV_OK) { + if (dwarf_siblingof(dwarf, 0, &cudie, &error) == DW_DLV_OK) { + Dwarf_Die die_mem = 0; + Dwarf_Die resultDie = find_fundie_by_pc( + fobj, cudie, addr, die_mem); + + if (resultDie) { + found = true; + break; + } + } + } + + if (found) { + while (dwarf_next_cu_header_d(dwarf, 1, 0, 0, 0, 0, 0, 0, 0, 0, + &next_cu_header, 0, &error) == DW_DLV_OK) { + // Reset the cu header state. Libdwarf's next_cu_header API + // keeps its own iterator per Dwarf_Debug that can't be reset. + // We need to keep fetching elements until the end. + } + } + + if (found) + return cudie; + + // We failed. + return NULL; + } +}; +#endif // BACKWARD_HAS_DWARF == 1 + +template<> +class TraceResolverImpl: + public TraceResolverLinuxImpl {}; + +#endif // BACKWARD_SYSTEM_LINUX + +#ifdef BACKWARD_SYSTEM_DARWIN + +template +class TraceResolverDarwinImpl; + +template <> +class TraceResolverDarwinImpl: + public TraceResolverImplBase { +public: + template + void load_stacktrace(ST& st) { + using namespace details; + if (st.size() == 0) { + return; + } + _symbols.reset( + backtrace_symbols(st.begin(), st.size()) + ); + } + + ResolvedTrace resolve(ResolvedTrace trace) { + // parse: + // + + char* filename = _symbols[trace.idx]; + + // skip " " + while(*filename && *filename != ' ') filename++; + while(*filename == ' ') filename++; + + // find start of from end ( may contain a space) + char* p = filename + strlen(filename) - 1; + // skip to start of " + " + while(p > filename && *p != ' ') p--; + while(p > filename && *p == ' ') p--; + while(p > filename && *p != ' ') p--; + while(p > filename && *p == ' ') p--; + char *funcname_end = p + 1; + + // skip to start of "" + while(p > filename && *p != ' ') p--; + char *funcname = p + 1; + + // skip to start of " " + while(p > filename && *p == ' ') p--; + while(p > filename && *p != ' ') p--; + while(p > filename && *p == ' ') p--; + + // skip "", handling the case where it contains a + char* filename_end = p + 1; + if (p == filename) { + // something went wrong, give up + filename_end = filename + strlen(filename); + funcname = filename_end; + } + trace.object_filename.assign(filename, filename_end); // ok even if filename_end is the ending \0 (then we assign entire string) + + if (*funcname) { // if it's not end of string + *funcname_end = '\0'; + + trace.object_function = this->demangle(funcname); + trace.object_function += " "; + trace.object_function += (funcname_end + 1); + trace.source.function = trace.object_function; // we cannot do better. + } + return trace; + } + +private: + details::handle _symbols; +}; + +template<> +class TraceResolverImpl: + public TraceResolverDarwinImpl {}; + +#endif // BACKWARD_SYSTEM_DARWIN + +class TraceResolver: + public TraceResolverImpl {}; + +/*************** CODE SNIPPET ***************/ + +class SourceFile { +public: + typedef std::vector > lines_t; + + SourceFile() {} + SourceFile(const std::string& path): _file(new std::ifstream(path.c_str())) {} + bool is_open() const { return _file->is_open(); } + + lines_t& get_lines(unsigned line_start, unsigned line_count, lines_t& lines) { + using namespace std; + // This function make uses of the dumbest algo ever: + // 1) seek(0) + // 2) read lines one by one and discard until line_start + // 3) read line one by one until line_start + line_count + // + // If you are getting snippets many time from the same file, it is + // somewhat a waste of CPU, feel free to benchmark and propose a + // better solution ;) + + _file->clear(); + _file->seekg(0); + string line; + unsigned line_idx; + + for (line_idx = 1; line_idx < line_start; ++line_idx) { + std::getline(*_file, line); + if (!*_file) { + return lines; + } + } + + // think of it like a lambda in C++98 ;) + // but look, I will reuse it two times! + // What a good boy am I. + struct isspace { + bool operator()(char c) { + return std::isspace(c); + } + }; + + bool started = false; + for (; line_idx < line_start + line_count; ++line_idx) { + getline(*_file, line); + if (!*_file) { + return lines; + } + if (!started) { + if (std::find_if(line.begin(), line.end(), + not_isspace()) == line.end()) + continue; + started = true; + } + lines.push_back(make_pair(line_idx, line)); + } + + lines.erase( + std::find_if(lines.rbegin(), lines.rend(), + not_isempty()).base(), lines.end() + ); + return lines; + } + + lines_t get_lines(unsigned line_start, unsigned line_count) { + lines_t lines; + return get_lines(line_start, line_count, lines); + } + + // there is no find_if_not in C++98, lets do something crappy to + // workaround. + struct not_isspace { + bool operator()(char c) { + return !std::isspace(c); + } + }; + // and define this one here because C++98 is not happy with local defined + // struct passed to template functions, fuuuu. + struct not_isempty { + bool operator()(const lines_t::value_type& p) { + return !(std::find_if(p.second.begin(), p.second.end(), + not_isspace()) == p.second.end()); + } + }; + + void swap(SourceFile& b) { + _file.swap(b._file); + } + +#ifdef BACKWARD_ATLEAST_CXX11 + SourceFile(SourceFile&& from): _file(nullptr) { + swap(from); + } + SourceFile& operator=(SourceFile&& from) { + swap(from); return *this; + } +#else + explicit SourceFile(const SourceFile& from) { + // some sort of poor man's move semantic. + swap(const_cast(from)); + } + SourceFile& operator=(const SourceFile& from) { + // some sort of poor man's move semantic. + swap(const_cast(from)); return *this; + } +#endif + +private: + details::handle + > _file; + +#ifdef BACKWARD_ATLEAST_CXX11 + SourceFile(const SourceFile&) = delete; + SourceFile& operator=(const SourceFile&) = delete; +#endif +}; + +class SnippetFactory { +public: + typedef SourceFile::lines_t lines_t; + + lines_t get_snippet(const std::string& filename, + unsigned line_start, unsigned context_size) { + + SourceFile& src_file = get_src_file(filename); + unsigned start = line_start - context_size / 2; + return src_file.get_lines(start, context_size); + } + + lines_t get_combined_snippet( + const std::string& filename_a, unsigned line_a, + const std::string& filename_b, unsigned line_b, + unsigned context_size) { + SourceFile& src_file_a = get_src_file(filename_a); + SourceFile& src_file_b = get_src_file(filename_b); + + lines_t lines = src_file_a.get_lines(line_a - context_size / 4, + context_size / 2); + src_file_b.get_lines(line_b - context_size / 4, context_size / 2, + lines); + return lines; + } + + lines_t get_coalesced_snippet(const std::string& filename, + unsigned line_a, unsigned line_b, unsigned context_size) { + SourceFile& src_file = get_src_file(filename); + + using std::min; using std::max; + unsigned a = min(line_a, line_b); + unsigned b = max(line_a, line_b); + + if ((b - a) < (context_size / 3)) { + return src_file.get_lines((a + b - context_size + 1) / 2, + context_size); + } + + lines_t lines = src_file.get_lines(a - context_size / 4, + context_size / 2); + src_file.get_lines(b - context_size / 4, context_size / 2, lines); + return lines; + } + + +private: + typedef details::hashtable::type src_files_t; + src_files_t _src_files; + + SourceFile& get_src_file(const std::string& filename) { + src_files_t::iterator it = _src_files.find(filename); + if (it != _src_files.end()) { + return it->second; + } + SourceFile& new_src_file = _src_files[filename]; + new_src_file = SourceFile(filename); + return new_src_file; + } +}; + +/*************** PRINTER ***************/ + +namespace ColorMode { + enum type { + automatic, + never, + always + }; +} + +class cfile_streambuf: public std::streambuf { +public: + cfile_streambuf(FILE *_sink): sink(_sink) {} + int_type underflow() override { return traits_type::eof(); } + int_type overflow(int_type ch) override { + if (traits_type::not_eof(ch) && fwrite(&ch, sizeof ch, 1, sink) == 1) { + return ch; + } + return traits_type::eof(); + } + + std::streamsize xsputn(const char_type* s, std::streamsize count) override { + return static_cast(fwrite(s, sizeof *s, static_cast(count), sink)); + } + +#ifdef BACKWARD_ATLEAST_CXX11 +public: + cfile_streambuf(const cfile_streambuf&) = delete; + cfile_streambuf& operator=(const cfile_streambuf&) = delete; +#else +private: + cfile_streambuf(const cfile_streambuf &); + cfile_streambuf &operator= (const cfile_streambuf &); +#endif + +private: + FILE *sink; + std::vector buffer; +}; + +#ifdef BACKWARD_SYSTEM_LINUX + +namespace Color { + enum type { + yellow = 33, + purple = 35, + reset = 39 + }; +} // namespace Color + +class Colorize { +public: + Colorize(std::ostream& os): + _os(os), _reset(false), _enabled(false) {} + + void activate(ColorMode::type mode) { + _enabled = mode == ColorMode::always; + } + + void activate(ColorMode::type mode, FILE* fp) { + activate(mode, fileno(fp)); + } + + void set_color(Color::type ccode) { + if (!_enabled) return; + + // I assume that the terminal can handle basic colors. Seriously I + // don't want to deal with all the termcap shit. + _os << "\033[" << static_cast(ccode) << "m"; + _reset = (ccode != Color::reset); + } + + ~Colorize() { + if (_reset) { + set_color(Color::reset); + } + } + +private: + void activate(ColorMode::type mode, int fd) { + activate(mode == ColorMode::automatic && isatty(fd) ? ColorMode::always : mode); + } + + std::ostream& _os; + bool _reset; + bool _enabled; +}; + +#else // ndef BACKWARD_SYSTEM_LINUX + +namespace Color { + enum type { + yellow = 0, + purple = 0, + reset = 0 + }; +} // namespace Color + +class Colorize { +public: + Colorize(std::ostream&) {} + void activate(ColorMode::type) {} + void activate(ColorMode::type, FILE*) {} + void set_color(Color::type) {} +}; + +#endif // BACKWARD_SYSTEM_LINUX + +class Printer { +public: + + bool snippet; + ColorMode::type color_mode; + bool address; + bool object; + int inliner_context_size; + int trace_context_size; + + Printer(): + snippet(true), + color_mode(ColorMode::automatic), + address(false), + object(false), + inliner_context_size(5), + trace_context_size(7) + {} + + template + FILE* print(ST& st, FILE* fp = stderr) { + cfile_streambuf obuf(fp); + std::ostream os(&obuf); + Colorize colorize(os); + colorize.activate(color_mode, fp); + print_stacktrace(st, os, colorize); + return fp; + } + + template + std::ostream& print(ST& st, std::ostream& os) { + Colorize colorize(os); + colorize.activate(color_mode); + print_stacktrace(st, os, colorize); + return os; + } + + template + FILE* print(IT begin, IT end, FILE* fp = stderr, size_t thread_id = 0) { + cfile_streambuf obuf(fp); + std::ostream os(&obuf); + Colorize colorize(os); + colorize.activate(color_mode, fp); + print_stacktrace(begin, end, os, thread_id, colorize); + return fp; + } + + template + std::ostream& print(IT begin, IT end, std::ostream& os, size_t thread_id = 0) { + Colorize colorize(os); + colorize.activate(color_mode); + print_stacktrace(begin, end, os, thread_id, colorize); + return os; + } + +private: + TraceResolver _resolver; + SnippetFactory _snippets; + + template + void print_stacktrace(ST& st, std::ostream& os, Colorize& colorize) { + print_header(os, st.thread_id()); + _resolver.load_stacktrace(st); + for (size_t trace_idx = st.size(); trace_idx > 0; --trace_idx) { + print_trace(os, _resolver.resolve(st[trace_idx-1]), colorize); + } + } + + template + void print_stacktrace(IT begin, IT end, std::ostream& os, size_t thread_id, Colorize& colorize) { + print_header(os, thread_id); + for (; begin != end; ++begin) { + print_trace(os, *begin, colorize); + } + } + + void print_header(std::ostream& os, size_t thread_id) { + os << "Stack trace (most recent call last)"; + if (thread_id) { + os << " in thread " << thread_id; + } + os << ":\n"; + } + + void print_trace(std::ostream& os, const ResolvedTrace& trace, + Colorize& colorize) { + os << "#" + << std::left << std::setw(2) << trace.idx + << std::right; + bool already_indented = true; + + if (!trace.source.filename.size() || object) { + os << " Object \"" + << trace.object_filename + << "\", at " + << trace.addr + << ", in " + << trace.object_function + << "\n"; + already_indented = false; + } + + for (size_t inliner_idx = trace.inliners.size(); + inliner_idx > 0; --inliner_idx) { + if (!already_indented) { + os << " "; + } + const ResolvedTrace::SourceLoc& inliner_loc + = trace.inliners[inliner_idx-1]; + print_source_loc(os, " | ", inliner_loc); + if (snippet) { + print_snippet(os, " | ", inliner_loc, + colorize, Color::purple, inliner_context_size); + } + already_indented = false; + } + + if (trace.source.filename.size()) { + if (!already_indented) { + os << " "; + } + print_source_loc(os, " ", trace.source, trace.addr); + if (snippet) { + print_snippet(os, " ", trace.source, + colorize, Color::yellow, trace_context_size); + } + } + } + + void print_snippet(std::ostream& os, const char* indent, + const ResolvedTrace::SourceLoc& source_loc, + Colorize& colorize, Color::type color_code, + int context_size) + { + using namespace std; + typedef SnippetFactory::lines_t lines_t; + + lines_t lines = _snippets.get_snippet(source_loc.filename, + source_loc.line, static_cast(context_size)); + + for (lines_t::const_iterator it = lines.begin(); + it != lines.end(); ++it) { + if (it-> first == source_loc.line) { + colorize.set_color(color_code); + os << indent << ">"; + } else { + os << indent << " "; + } + os << std::setw(4) << it->first + << ": " + << it->second + << "\n"; + if (it-> first == source_loc.line) { + colorize.set_color(Color::reset); + } + } + } + + void print_source_loc(std::ostream& os, const char* indent, + const ResolvedTrace::SourceLoc& source_loc, + void* addr=nullptr) { + os << indent + << "Source \"" + << source_loc.filename + << "\", line " + << source_loc.line + << ", in " + << source_loc.function; + + if (address && addr != nullptr) { + os << " [" << addr << "]"; + } + os << "\n"; + } +}; + +/*************** SIGNALS HANDLING ***************/ + +#if defined(BACKWARD_SYSTEM_LINUX) || defined(BACKWARD_SYSTEM_DARWIN) + + +class SignalHandling { +public: + static std::vector make_default_signals() { + const int posix_signals[] = { + // Signals for which the default action is "Core". + SIGABRT, // Abort signal from abort(3) + SIGBUS, // Bus error (bad memory access) + SIGFPE, // Floating point exception + SIGILL, // Illegal Instruction + SIGIOT, // IOT trap. A synonym for SIGABRT + SIGQUIT, // Quit from keyboard + SIGSEGV, // Invalid memory reference + SIGSYS, // Bad argument to routine (SVr4) + SIGTRAP, // Trace/breakpoint trap + SIGXCPU, // CPU time limit exceeded (4.2BSD) + SIGXFSZ, // File size limit exceeded (4.2BSD) +#if defined(BACKWARD_SYSTEM_DARWIN) + SIGEMT, // emulation instruction executed +#endif + }; + return std::vector(posix_signals, posix_signals + sizeof posix_signals / sizeof posix_signals[0] ); + } + + SignalHandling(const std::vector& posix_signals = make_default_signals()): + _loaded(false) { + bool success = true; + + const size_t stack_size = 1024 * 1024 * 8; + _stack_content.reset(static_cast(malloc(stack_size))); + if (_stack_content) { + stack_t ss; + ss.ss_sp = _stack_content.get(); + ss.ss_size = stack_size; + ss.ss_flags = 0; + if (sigaltstack(&ss, nullptr) < 0) { + success = false; + } + } else { + success = false; + } + + for (size_t i = 0; i < posix_signals.size(); ++i) { + struct sigaction action; + memset(&action, 0, sizeof action); + action.sa_flags = static_cast(SA_SIGINFO | SA_ONSTACK | SA_NODEFER | + SA_RESETHAND); + sigfillset(&action.sa_mask); + sigdelset(&action.sa_mask, posix_signals[i]); +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdisabled-macro-expansion" +#endif + action.sa_sigaction = &sig_handler; +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + + int r = sigaction(posix_signals[i], &action, nullptr); + if (r < 0) success = false; + } + + _loaded = success; + } + + bool loaded() const { return _loaded; } + + static void handleSignal(int, siginfo_t* info, void* _ctx) { + ucontext_t *uctx = static_cast(_ctx); + + StackTrace st; + void* error_addr = nullptr; +#ifdef REG_RIP // x86_64 + error_addr = reinterpret_cast(uctx->uc_mcontext.gregs[REG_RIP]); +#elif defined(REG_EIP) // x86_32 + error_addr = reinterpret_cast(uctx->uc_mcontext.gregs[REG_EIP]); +#elif defined(__arm__) + error_addr = reinterpret_cast(uctx->uc_mcontext.arm_pc); +#elif defined(__aarch64__) + error_addr = reinterpret_cast(uctx->uc_mcontext.pc); +#elif defined(__ppc__) || defined(__powerpc) || defined(__powerpc__) || defined(__POWERPC__) + error_addr = reinterpret_cast(uctx->uc_mcontext.regs->nip); +#elif defined(__s390x__) + error_addr = reinterpret_cast(uctx->uc_mcontext.psw.addr); +#elif defined(__APPLE__) && defined(__x86_64__) + error_addr = reinterpret_cast(uctx->uc_mcontext->__ss.__rip); +#elif defined(__APPLE__) + error_addr = reinterpret_cast(uctx->uc_mcontext->__ss.__eip); +#else +# warning ":/ sorry, ain't know no nothing none not of your architecture!" +#endif + if (error_addr) { + st.load_from(error_addr, 32); + } else { + st.load_here(32); + } + + Printer printer; + printer.address = true; + printer.print(st, stderr); + +#if _XOPEN_SOURCE >= 700 || _POSIX_C_SOURCE >= 200809L + psiginfo(info, nullptr); +#else + (void)info; +#endif + } + +private: + details::handle _stack_content; + bool _loaded; + +#ifdef __GNUC__ + __attribute__((noreturn)) +#endif + static void sig_handler(int signo, siginfo_t* info, void* _ctx) { + handleSignal(signo, info, _ctx); + + // try to forward the signal. + raise(info->si_signo); + + // terminate the process immediately. + puts("watf? exit"); + _exit(EXIT_FAILURE); + } +}; + +#endif // BACKWARD_SYSTEM_LINUX || BACKWARD_SYSTEM_DARWIN + +#ifdef BACKWARD_SYSTEM_UNKNOWN + +class SignalHandling { +public: + SignalHandling(const std::vector& = std::vector()) {} + bool init() { return false; } + bool loaded() { return false; } +}; + +#endif // BACKWARD_SYSTEM_UNKNOWN + +} // namespace backward + +#endif /* H_GUARD */ diff --git a/external/open-iconic/ICON-LICENSE b/external/open-iconic/ICON-LICENSE new file mode 100644 index 0000000..2199f4a --- /dev/null +++ b/external/open-iconic/ICON-LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Waybury + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/external/open-iconic/check.svg b/external/open-iconic/check.svg new file mode 100644 index 0000000..f98d675 --- /dev/null +++ b/external/open-iconic/check.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/external/open-iconic/envelope-closed.svg b/external/open-iconic/envelope-closed.svg new file mode 100644 index 0000000..9fb9588 --- /dev/null +++ b/external/open-iconic/envelope-closed.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/external/open-iconic/envelope-open.svg b/external/open-iconic/envelope-open.svg new file mode 100644 index 0000000..b67740f --- /dev/null +++ b/external/open-iconic/envelope-open.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/external/open-iconic/minus.svg b/external/open-iconic/minus.svg new file mode 100644 index 0000000..35bbada --- /dev/null +++ b/external/open-iconic/minus.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/external/open-iconic/plus.svg b/external/open-iconic/plus.svg new file mode 100644 index 0000000..7ea42b3 --- /dev/null +++ b/external/open-iconic/plus.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/external/open-iconic/x.svg b/external/open-iconic/x.svg new file mode 100644 index 0000000..82ad290 --- /dev/null +++ b/external/open-iconic/x.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/external/static-todo/static_todo.hpp b/external/static-todo/static_todo.hpp new file mode 100644 index 0000000..ab163b7 --- /dev/null +++ b/external/static-todo/static_todo.hpp @@ -0,0 +1,78 @@ +/* +MIT License + +Copyright (c) 2018 Aurelien Regat-Barrel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once + +// Display errors only when testing +#ifndef STATIC_TODO_ENABLED + +#define TODO_BEFORE(month, year, msg) +#define FIXME_BEFORE(month, year, msg) + +#else + +constexpr int current_build_year() { + // example: "Nov 27 2018" + const char *year = __DATE__; + + return (year[7] - '0') * 1000 + (year[8] - '0') * 100 + (year[9] - '0') * 10 + (year[10] - '0'); +} + +constexpr int current_build_month() { + constexpr const char months[12][4]{ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + + constexpr const char date[] = __DATE__; + + for (int i = 0; i < 12; i++) { + const char *m = months[i]; + if (m[0] == date[0] && m[1] == date[1] && m[2] == date[2]) + return i + 1; + } + + return 0xFFFFFFFF; +} + +/// TODO_BEFORE() inserts a compilation "time bomb" that will trigger a "TODO" build error a soon as +/// the given date is reached. +/// +/// This is useful to force attention on a specific piece of code that should not been forgotten +/// among a growing list of many other "TODO" comments... +/// +/// Example: +/// TODO_BEFORE(01, 2019, "refactor to use std::optional<> once we compile in C++17 mode"); +#define TODO_BEFORE(month, year, msg) \ + static_assert((month > 0 && month <= 12) && \ + (current_build_year() < year || \ + (current_build_year() == year && current_build_month() < month)), \ + "TODO: " msg) + +/// FIXME_BEFORE() works the same way than TODO_BEFORE() but triggers a "FIXME" error instead +#define FIXME_BEFORE(month, year, msg) \ + static_assert((month > 0 && month <= 12) && \ + (current_build_year() < year || \ + (current_build_year() == year && current_build_month() < month)), \ + "FIXME: " msg) + +#endif diff --git a/external/use-backward-cpp.cmake b/external/use-backward-cpp.cmake new file mode 100644 index 0000000..6395de3 --- /dev/null +++ b/external/use-backward-cpp.cmake @@ -0,0 +1,24 @@ +function(get_all_targets _result _dir) + get_property(_subdirs DIRECTORY "${_dir}" PROPERTY SUBDIRECTORIES) + foreach(_subdir IN LISTS _subdirs) + get_all_targets(${_result} "${_subdir}") + endforeach() + get_property(_sub_targets DIRECTORY "${_dir}" PROPERTY BUILDSYSTEM_TARGETS) + set(${_result} ${${_result}} ${_sub_targets} PARENT_SCOPE) +endfunction() + +function(enable_backward_cpp) + add_subdirectory(external/backward-cpp) + get_all_targets(TARGET_LIST "${CMAKE_SOURCE_DIR}") + message("-- Target list: ${TARGET_LIST}") + list(FILTER TARGET_LIST EXCLUDE REGEX "backward_object|backward|coverage_report|fmt|fmt-header-only|link_target") + foreach(target IN ITEMS ${TARGET_LIST}) + add_backward(${target}) + target_sources(${target} PUBLIC ${BACKWARD_ENABLE}) + endforeach() +endfunction() + +string(TOLOWER "${CMAKE_BUILD_TYPE}" cmake_build_type_tolower) +if ("${cmake_build_type_tolower}" STREQUAL "debug") + #enable_backward_cpp() +endif () diff --git a/main.cpp b/main.cpp deleted file mode 100644 index e4efc1f..0000000 --- a/main.cpp +++ /dev/null @@ -1,211 +0,0 @@ -/* - * main.cpp - * - * Created on: 8 Oct 2016 - * Author: muttley - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "VDF.h" -#include "Filesystem.h" -#include "Settings.h" -#include "Utils.h" -#include "Logger.h" - -#include "MainWindow.h" - -#ifdef __APPLE__ - #include -#endif - -using namespace std; - -int main(int argc, char *argv[]) -{ - //Checking parameters - for (int i = 0; i < argc; i++) - { - if (strcmp(argv[i], "--verbose") == 0 || strcmp(argv[i], "-v") == 0) - LogLevel = 0; - if (strcmp(argv[i], "--preset-to-run") == 0) - { - if (i + 1 < argc) - Settings::PresetToRun = argv[i + 1]; - } - } - - LOG(1, "ArmA 3 Unix Launcher started"); - - //~/.config/a3unixlauncher - if (!Filesystem::DirectoryExists(Filesystem::HomeDirectory - + Filesystem::LauncherSettingsDirectory)) - { - bool result = Filesystem::CreateDirectory(Filesystem::HomeDirectory - + Filesystem::LauncherSettingsDirectory); - if (!result) - return 1; - } - - if (!Filesystem::FileExists(Filesystem::HomeDirectory - + Filesystem::LauncherSettingsDirectory - + Filesystem::LauncherSettingsFilename)) - { - Settings::Save(Filesystem::HomeDirectory - + Filesystem::LauncherSettingsDirectory - + Filesystem::LauncherSettingsFilename); - } - - Settings::Load(Filesystem::HomeDirectory - + Filesystem::LauncherSettingsDirectory - + Filesystem::LauncherSettingsFilename); - - - if (Settings::ArmaPath == Filesystem::DIR_NOT_FOUND) - Settings::ArmaPath = Filesystem::GetDirectory(DirectoryToFind::ArmaInstall); - - if (Settings::WorkshopPath == Filesystem::DIR_NOT_FOUND) - Settings::WorkshopPath = Filesystem::GetDirectory(DirectoryToFind::WorkshopMods); - - for (int i = 0; i < argc; i++) - { - if (strcmp(argv[i], "--purge") == 0) - { - if (Settings::ArmaPath != Filesystem::DIR_NOT_FOUND) - { - string removeWorkshop = "rm -rf " + Utils::BashAdaptPath(Settings::ArmaPath + Filesystem::ArmaDirWorkshop); - string removeCustom = "rm -rf " + Utils::BashAdaptPath(Settings::ArmaPath + Filesystem::ArmaDirCustom); - system(removeWorkshop.c_str()); - system(removeCustom.c_str()); - } - string removeConfig = "rm -rf \"" + Filesystem::HomeDirectory + Filesystem::LauncherSettingsDirectory + "\""; - system(removeConfig.c_str()); - - LOG(1, "Purged config file, !workshop, !custom directories"); - exit(0); - } - } - - //Dirty fix to Gtk::Application trying to parse arguments on its own - argc = 0; - Glib::RefPtr app = Gtk::Application::create(argc, argv, "muttley.a3unixlauncher"); - - #ifdef __APPLE__ - //apple path is like: /Applications/arma3-unix-launcher.app/Contents/MacOS/./arma3-unix-launcher - char *path = new char[4096]; - unsigned int pathLength = 4095; - _NSGetExecutablePath(path, &pathLength); - string currentPath = path; - delete[] path; - - string MainFormPath = Utils::RemoveLastElement(currentPath, false) + "MainForm.glade"; - #else - string MainFormPath = "/usr/share/arma3-unix-launcher/MainForm.glade"; - for (int i = 0; i < 2; i++) - { - if (!Filesystem::FileExists(MainFormPath)) - { - string binaryPath = Filesystem::GetSymlinkTarget("/proc/" + to_string(getpid()) + "/exe"); - MainFormPath = Utils::RemoveLastElement(binaryPath, false, i + 1) + "MainForm.glade"; - } - } - #endif - - Glib::RefPtr builder = Gtk::Builder::create_from_file(MainFormPath); - - cout << "GTK+ version: " << gtk_major_version << "." << gtk_minor_version << "." << gtk_micro_version << endl - << "Glib version: " << glib_major_version << "." << glib_minor_version << "." << glib_micro_version << endl; - - //if autodetection fails - while (Settings::ArmaPath == Filesystem::DIR_NOT_FOUND) - { - string Message = string("Launcher couldn't detect ArmA 3 installation directory") + - "\nClick 'Yes' to select appropriate directory" + - "\nClick 'No' to close the program"; - - Gtk::MessageDialog msg(Message, false, Gtk::MESSAGE_INFO, Gtk::BUTTONS_YES_NO); - int result = msg.run(); - - if (result == Gtk::RESPONSE_NO) - exit(2); - - Gtk::FileChooserDialog fcDialog("Select ArmA 3 install directory", Gtk::FILE_CHOOSER_ACTION_SELECT_FOLDER); - fcDialog.add_button("_Open", 1); - result = fcDialog.run(); - if (result == 1) - { - string currentFolder = fcDialog.get_current_folder(); - if (Filesystem::FileExists(currentFolder + "/arma3.x86_64") - || Filesystem::FileExists(currentFolder + "/arma3_x64.exe") - || Filesystem::FileExists(currentFolder + "/ArmA3.app") - || Filesystem::DirectoryExists(currentFolder + "/ArmA3.app")) - Settings::ArmaPath = currentFolder; - currentFolder = fcDialog.get_filename(); - if (Filesystem::FileExists(currentFolder + "/arma3.x86_64") - || Filesystem::FileExists(currentFolder + "/arma3_x64.exe") - || Filesystem::FileExists(currentFolder + "/ArmA3.app") - || Filesystem::DirectoryExists(currentFolder + "/ArmA3.app")) - Settings::ArmaPath = currentFolder; - - if (Settings::ArmaPath == Filesystem::DIR_NOT_FOUND) - { - string Message2 = string("Selected directory seems incorrect") + - "\n" + currentFolder + "/arma3_x64.exe" - "\n" + currentFolder + "/arma3.x86_64 doesn't exist" - "\n" + currentFolder + "/ArmA3.app doesn't exist"; - - Gtk::MessageDialog msg2(Message2); - msg2.run(); - } - else - { - std::string estimated_workshop_path = Utils::RemoveLastElement(Settings::ArmaPath, false, 2) + "workshop/content/107410"; - LOG(1, "Estimated workshop path: " + estimated_workshop_path); - if (Filesystem::DirectoryExists((estimated_workshop_path))) - { - char *resolved_path = realpath(estimated_workshop_path.c_str(), nullptr); - Settings::WorkshopPath = resolved_path; - free(resolved_path); - LOG(1, "Workaround workshop path: " + Settings::WorkshopPath); - } - else - LOG(0, "Estimated workshop path does not exist!"); - } - } - } - - if (Filesystem::IsProton(Settings::ArmaPath)) - { - Filesystem::ArmaConfigFile = Settings::ArmaPath + "/../../compatdata/107410/pfx/drive_c/users/steamuser/My Documents/Arma 3/Arma3.cfg"; - LOG(0, "Proton detected"); - - LOG(1, "Config path: " + Filesystem::ArmaConfigFile); - } - else - { - LOG(0, "IS NOT PROTON"); - } - - MainWindow *mainWindow = nullptr; - - builder->get_widget_derived("MainForm", mainWindow); - - if (mainWindow) - app->run(*mainWindow); - - delete mainWindow; - - return 0; -} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..ce464df --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(arma3-unix-launcher-library) +add_subdirectory(arma3-unix-launcher) diff --git a/src/arma3-unix-launcher-library/CMakeLists.txt b/src/arma3-unix-launcher-library/CMakeLists.txt new file mode 100644 index 0000000..cb15b33 --- /dev/null +++ b/src/arma3-unix-launcher-library/CMakeLists.txt @@ -0,0 +1,5 @@ +file(GLOB_RECURSE SOURCES *.cpp *.hpp) + +add_library(arma3-unix-launcher-library STATIC ${SOURCES}) +target_include_directories(arma3-unix-launcher-library INTERFACE .) +target_link_libraries(arma3-unix-launcher-library stdc++fs fmt::fmt nlohmann::json) diff --git a/src/arma3-unix-launcher-library/arma3client.cpp b/src/arma3-unix-launcher-library/arma3client.cpp new file mode 100644 index 0000000..8b262cb --- /dev/null +++ b/src/arma3-unix-launcher-library/arma3client.cpp @@ -0,0 +1,180 @@ +#include "arma3client.hpp" + +#include +#include +#include + +#include +#include + +#include + +#include "cppfilter.hpp" +#include "filesystem_utils.hpp" +#include "std_utils.hpp" +#include "string_utils.hpp" + +#include "exceptions/directory_not_found.hpp" +#include "exceptions/file_no_access.hpp" +#include "exceptions/file_not_found.hpp" +#include "exceptions/not_a_symlink.hpp" +#include "exceptions/syntax_error.hpp" + +using namespace std; +using namespace std::filesystem; + +using namespace StdUtils; +using namespace StringUtils; + +namespace fs = FilesystemUtils; + +namespace ARMA3 +{ + Client::Client(std::filesystem::path const &arma_path, std::filesystem::path const &target_workshop_path) + { + path_ = arma_path; + for (auto const &executable : Definitions::executable_names) + { + if (fs::Exists(path_ / executable)) + { + path_executable_ = path_ / executable; + break; + } + } + if (path_executable_.empty()) + throw FileNotFoundException("arma3.exe"); + path_workshop_target_ = target_workshop_path; + } + + void Client::CreateArmaCfg(vector const &workshop_mod_ids, vector const &custom_mods, path cfg_path) + { + if (cfg_path.empty()) + cfg_path = GetCfgPath(); + if (!fs::Exists(cfg_path)) + { + if (!fs::Exists(cfg_path.parent_path())) + fs::CreateDirectories(cfg_path.parent_path()); + StdUtils::CreateFile(cfg_path); + } + std::string existing_config = FileReadAllText(cfg_path); + + CppFilter cpp_filter{existing_config}; + auto stripped_config = cpp_filter.RemoveClass("class ModLauncherList"); + stripped_config += R"cpp(class ModLauncherList +{ +)cpp"; + + int mod_number = 1; + for (auto const& mod_id : workshop_mod_ids) + stripped_config += GenerateCfgCppForMod(GetPathWorkshop() / mod_id, mod_number++); + for (auto const& mod_path : custom_mods) + stripped_config += GenerateCfgCppForMod(mod_path, mod_number++); + + stripped_config += "};\n"; + + FileWriteAllText(cfg_path, stripped_config); + } + + bool Client::IsProton() + { + return path_executable_.filename() == "arma3_x64.exe"; + } + + void Client::Start(string const &arguments) + { + #ifdef __linux + if (IsProton()) + StdUtils::StartBackgroundProcess("steam -applaunch 107410 -nolauncher " + arguments); + else + StdUtils::StartBackgroundProcess("steam -applaunch 107410 " + arguments); + #else + std::string launch_command = "open steam://run/107410//" + StringUtils::Replace(arguments, " ", "%20"); + StdUtils::StartBackgroundProcess(launch_command); + #endif + } + + std::vector Client::GetHomeMods() + { + return GetModsFromDirectory(GetPath()); + } + + std::vector Client::GetWorkshopMods() + { + return GetModsFromDirectory(GetPathWorkshop()); + } + + std::filesystem::path Client::GetCfgPath() + { + return std::filesystem::path(Definitions::home_directory) + / Definitions::local_share_prefix + / Definitions::bohemia_interactive_prefix + / Definitions::game_config_path; + } + + char Client::GetFakeDriveLetter() + { + return IsProton() ? 'Z' : 'C'; + } + + string Client::GenerateCfgCppForMod(path const &mod_path, int mod_index) + { + constexpr char const *mod_template = + R"cpp( class Mod{} + {{ + dir="{}"; + name="{}"; + origin="GAME DIR"; + fullPath="{}"; + }}; +)cpp"; + + Mod mod(mod_path); + path mod_path_absolute = trim(mod.path_.string(), "\""); + path final_path = StringUtils::ToWindowsPath(mod_path_absolute, GetFakeDriveLetter()); + path dir = trim(mod_path_absolute.filename().string(), "\""); + auto name = mod.GetValueOrReturnDefault(dir, "name", "dir", "tooltip", "name_read_failed"); + + return fmt::format(mod_template, mod_index, dir.string(), name, final_path.string()); + } + + std::filesystem::path const &Client::GetPath() + { + return path_; + } + + std::filesystem::path const &Client::GetPathExecutable() + { + return path_executable_; + } + + std::filesystem::path const &Client::GetPathWorkshop() + { + return path_workshop_target_; + } + + std::vector Client::GetModsFromDirectory(path const &dir) + { + std::vector ret; + + for (auto const &ent : fs::Ls(dir)) + { + if (StdUtils::Contains(Definitions::exclusions, ent)) + continue; + + std::filesystem::path mod_dir = dir / ent; + if (!fs::IsDirectory(mod_dir)) + continue; + if (!StdUtils::Contains(fs::Ls(mod_dir, true), "addons")) + continue; + + ret.emplace_back(mod_dir); + } + + std::sort(ret.begin(), ret.end(), [](Mod const & m1, Mod const & m2) + { + return m1.GetValueOrReturnDefault("name", "dir", StringUtils::Lowercase(m1.path_)) < m2.GetValueOrReturnDefault("name", "dir", StringUtils::Lowercase(m2.path_)); + }); + + return ret; + } +} diff --git a/src/arma3-unix-launcher-library/arma3client.hpp b/src/arma3-unix-launcher-library/arma3client.hpp new file mode 100644 index 0000000..214c23e --- /dev/null +++ b/src/arma3-unix-launcher-library/arma3client.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +#include "mod.hpp" + +namespace ARMA3::Definitions +{ + static std::string home_directory = getenv("HOME"); + + static constexpr const char *app_id = "107410"; + + static const std::array exclusions{"Addons", "Argo", "BattlEye", "Contact", "Curator", "Dll", "Dta", "Enoch", "Expansion", "fontconfig", "Heli", "Jets", "Kart", "Keys", "Launcher", "MPMissions", "Mark", "Missions", "Orange", "Tacops", "Tank", "legal", "steam_shader_cache"}; + + #ifdef __linux + static constexpr std::array const executable_names {"arma3.x86_64", "arma3_x64.exe"}; + static constexpr char const *local_share_prefix = ".local/share"; + static constexpr char const *bohemia_interactive_prefix = "bohemiainteractive/arma3"; + static constexpr char const *game_config_path = "GameDocuments/Arma 3/Arma3.cfg"; + #else //__APPLE__ + static constexpr std::array const executable_names {"ArmA3.app"}; + static constexpr char const *local_share_prefix = "Library/Application Support"; + static constexpr char const *bohemia_interactive_prefix = "com.vpltd.Arma3"; + static constexpr char const *game_config_path = "GameDocuments/Arma 3/Arma3.cfg"; + #endif +} + +namespace ARMA3 +{ + class Client + { + public: + Client(std::filesystem::path const &arma_path, std::filesystem::path const &target_workshop_path); + + void CreateArmaCfg(std::vector const &workshop_mod_ids, + std::vector const &custom_mods, std::filesystem::path cfg_path = ""); + bool IsProton(); + void Start(std::string const &arguments); + + std::vector GetHomeMods(); + std::vector GetWorkshopMods(); + + std::filesystem::path const &GetPath(); + std::filesystem::path const &GetPathExecutable(); + std::filesystem::path const &GetPathWorkshop(); + + private: + std::vector GetModsFromDirectory(std::filesystem::path const &dir); + std::filesystem::path GetCfgPath(); + char GetFakeDriveLetter(); + std::string GenerateCfgCppForMod(std::filesystem::path const &path, int mod_index); + + std::filesystem::path path_; + std::filesystem::path path_custom_; + std::filesystem::path path_executable_; + std::filesystem::path path_workshop_local_; + std::filesystem::path path_workshop_target_; + }; +} diff --git a/src/arma3-unix-launcher-library/cppfilter.cpp b/src/arma3-unix-launcher-library/cppfilter.cpp new file mode 100644 index 0000000..d96eb6c --- /dev/null +++ b/src/arma3-unix-launcher-library/cppfilter.cpp @@ -0,0 +1,108 @@ +/* + * Every project has this dark place, where code gets dirty + * this is the place, tread carefeully + */ + +#include "cppfilter.hpp" + +#include +#include + +#include "exceptions/syntax_error.hpp" + +std::string CppFilter::RemoveClass(std::string const &class_name) +{ + auto occurences = FindAllClassOccurences(class_name); + if (occurences.size() == 0) + return class_text_; + + std::string ret = class_text_; + + for (int i = static_cast(occurences.size()) - 1; i >= 0; --i) + { + auto boundaries = GetClassBoundaries(class_name, occurences[i]); + ret = ret.substr(0, boundaries.first) + ret.substr(boundaries.second); + } + + return ret; +} + +std::vector CppFilter::FindAllClassOccurences(std::string const &class_name) +{ + std::vector ret; + size_t pos = class_text_.find(class_name); + while (pos != std::string::npos) + { + ret.push_back(pos); + pos = class_text_.find(class_name, pos + class_name.size()); + } + return ret; +} + +std::pair CppFilter::GetClassBoundaries(std::string const &class_name, size_t start) +{ + std::string_view view(class_text_.c_str() + start, class_name.size()); + if (view != class_name) + throw SyntaxErrorException("Cannot find class name"); + + int bracket_depth = 1; + size_t bracket_pos = class_text_.find('{', start); + if (bracket_pos == std::string::npos) + throw SyntaxErrorException("Cannot find opening bracket"); + + size_t pos = bracket_pos + 1; + + bool escape = false; + bool in_string = false; + while (pos < class_text_.size() && bracket_depth > 0) + { + char c = class_text_[pos]; + + if (escape) + escape = false; + else if (in_string && c == '\\') + escape = true; + else if (c == '"') + in_string = !in_string; + else if (!in_string) + { + if (c == '{') + ++bracket_depth; + else if (c == '}') + --bracket_depth; + } + ++pos; + } + + if (bracket_depth != 0) + throw SyntaxErrorException("Unclosed bracket"); + + return std::make_pair(start, GetColonNewlineOrChar(pos)); +} + +size_t CppFilter::GetColonNewlineOrChar(size_t pos) +{ + bool colon_found = false; + bool newline_found = false; + bool char_found = false; + + while (pos < class_text_.size()) + { + char c = class_text_[pos]; + if (c == ';') + colon_found = true; + else if (c == '\n') + newline_found = true; + else if (isalnum(c)) + char_found = true; + + if (colon_found && newline_found) + return pos + 1; + else if (colon_found && char_found) + return pos; + + ++pos; + } + + return std::string::npos; +} diff --git a/src/arma3-unix-launcher-library/cppfilter.hpp b/src/arma3-unix-launcher-library/cppfilter.hpp new file mode 100644 index 0000000..c2ee217 --- /dev/null +++ b/src/arma3-unix-launcher-library/cppfilter.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +class CppFilter +{ + public: + std::string RemoveClass(std::string const &class_name); + + std::string class_text_; + + private: + std::vector FindAllClassOccurences(std::string const &class_name); + std::pair GetClassBoundaries(std::string const &class_name, size_t start); + size_t GetColonNewlineOrChar(size_t pos); +}; diff --git a/src/arma3-unix-launcher-library/exceptions/directory_no_access.cpp b/src/arma3-unix-launcher-library/exceptions/directory_no_access.cpp new file mode 100644 index 0000000..d2fe096 --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/directory_no_access.cpp @@ -0,0 +1,11 @@ +#include "directory_no_access.hpp" + +DirectoryNoAccessException::DirectoryNoAccessException(std::string const &path) : message("Cannot access directory" + + path) +{ +} + +const char *DirectoryNoAccessException::what() const noexcept +{ + return message.c_str(); +} diff --git a/src/arma3-unix-launcher-library/exceptions/directory_no_access.hpp b/src/arma3-unix-launcher-library/exceptions/directory_no_access.hpp new file mode 100644 index 0000000..cfd308d --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/directory_no_access.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class DirectoryNoAccessException : public std::exception +{ + public: + explicit DirectoryNoAccessException(std::string const &path); + + const char *what() const noexcept override; + + private: + std::string message; +}; diff --git a/src/arma3-unix-launcher-library/exceptions/directory_not_found.cpp b/src/arma3-unix-launcher-library/exceptions/directory_not_found.cpp new file mode 100644 index 0000000..ad3df4e --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/directory_not_found.cpp @@ -0,0 +1,11 @@ +#include "directory_not_found.hpp" + +DirectoryNotFoundException::DirectoryNotFoundException(std::string const &path) : message("Directory not found: " + + path) +{ +} + +const char *DirectoryNotFoundException::what() const noexcept +{ + return message.c_str(); +} diff --git a/src/arma3-unix-launcher-library/exceptions/directory_not_found.hpp b/src/arma3-unix-launcher-library/exceptions/directory_not_found.hpp new file mode 100644 index 0000000..64efb5f --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/directory_not_found.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class DirectoryNotFoundException : public std::exception +{ + public: + explicit DirectoryNotFoundException(std::string const &path); + + const char *what() const noexcept override; + + private: + std::string message; +}; diff --git a/src/arma3-unix-launcher-library/exceptions/file_no_access.cpp b/src/arma3-unix-launcher-library/exceptions/file_no_access.cpp new file mode 100644 index 0000000..97b4cd3 --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/file_no_access.cpp @@ -0,0 +1,10 @@ +#include "file_no_access.hpp" + +FileNoAccessException::FileNoAccessException(std::string const &path): message("Cannot access file: " + path) +{ +} + +const char *FileNoAccessException::what() const noexcept +{ + return message.c_str(); +} diff --git a/src/arma3-unix-launcher-library/exceptions/file_no_access.hpp b/src/arma3-unix-launcher-library/exceptions/file_no_access.hpp new file mode 100644 index 0000000..2bab5d8 --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/file_no_access.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class FileNoAccessException : public std::exception +{ + public: + explicit FileNoAccessException(std::string const &path); + + const char *what() const noexcept override; + + private: + std::string message; +}; diff --git a/src/arma3-unix-launcher-library/exceptions/file_not_found.cpp b/src/arma3-unix-launcher-library/exceptions/file_not_found.cpp new file mode 100644 index 0000000..edb5a93 --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/file_not_found.cpp @@ -0,0 +1,10 @@ +#include "file_not_found.hpp" + +FileNotFoundException::FileNotFoundException(std::string const &path) : message("File not found: " + path) +{ +} + +const char *FileNotFoundException::what() const noexcept +{ + return message.c_str(); +} diff --git a/src/arma3-unix-launcher-library/exceptions/file_not_found.hpp b/src/arma3-unix-launcher-library/exceptions/file_not_found.hpp new file mode 100644 index 0000000..6713748 --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/file_not_found.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class FileNotFoundException : public std::exception +{ + public: + explicit FileNotFoundException(std::string const &path); + + const char *what() const noexcept override; + + private: + std::string message; +}; diff --git a/src/arma3-unix-launcher-library/exceptions/not_a_directory.cpp b/src/arma3-unix-launcher-library/exceptions/not_a_directory.cpp new file mode 100644 index 0000000..b0fbc72 --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/not_a_directory.cpp @@ -0,0 +1,10 @@ +#include "not_a_directory.hpp" + +NotADirectoryException::NotADirectoryException(std::string const &path): message("Not a directory: " + path) +{ +} + +const char *NotADirectoryException::what() const noexcept +{ + return message.c_str(); +} diff --git a/src/arma3-unix-launcher-library/exceptions/not_a_directory.hpp b/src/arma3-unix-launcher-library/exceptions/not_a_directory.hpp new file mode 100644 index 0000000..e9707d1 --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/not_a_directory.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class NotADirectoryException : public std::exception +{ + public: + explicit NotADirectoryException(std::string const &path); + + const char *what() const noexcept override; + + private: + std::string message; +}; diff --git a/src/arma3-unix-launcher-library/exceptions/not_a_symlink.cpp b/src/arma3-unix-launcher-library/exceptions/not_a_symlink.cpp new file mode 100644 index 0000000..e67aa54 --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/not_a_symlink.cpp @@ -0,0 +1,10 @@ +#include "not_a_symlink.hpp" + +NotASymlinkException::NotASymlinkException(std::string const &path): message("Not a symlink: " + path) +{ +} + +const char *NotASymlinkException::what() const noexcept +{ + return message.c_str(); +} diff --git a/src/arma3-unix-launcher-library/exceptions/not_a_symlink.hpp b/src/arma3-unix-launcher-library/exceptions/not_a_symlink.hpp new file mode 100644 index 0000000..a3562a5 --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/not_a_symlink.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class NotASymlinkException : public std::exception +{ + public: + explicit NotASymlinkException(std::string const &path); + + const char *what() const noexcept override; + + private: + std::string message; +}; diff --git a/src/arma3-unix-launcher-library/exceptions/path_no_access.cpp b/src/arma3-unix-launcher-library/exceptions/path_no_access.cpp new file mode 100644 index 0000000..311b995 --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/path_no_access.cpp @@ -0,0 +1,10 @@ +#include "path_no_access.hpp" + +PathNoAccessException::PathNoAccessException(std::string const &path) : message("Cannot access path: " + path) +{ +} + +const char *PathNoAccessException::PathNoAccessException::what() const noexcept +{ + return message.c_str(); +} diff --git a/src/arma3-unix-launcher-library/exceptions/path_no_access.hpp b/src/arma3-unix-launcher-library/exceptions/path_no_access.hpp new file mode 100644 index 0000000..65b6def --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/path_no_access.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class PathNoAccessException : public std::exception +{ + public: + explicit PathNoAccessException(std::string const &path); + + const char *what() const noexcept override; + + private: + std::string message; +}; diff --git a/src/arma3-unix-launcher-library/exceptions/steam_install_not_found.cpp b/src/arma3-unix-launcher-library/exceptions/steam_install_not_found.cpp new file mode 100644 index 0000000..51ea134 --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/steam_install_not_found.cpp @@ -0,0 +1,6 @@ +#include "steam_install_not_found.hpp" + +const char *SteamInstallNotFoundException::what() const noexcept +{ + return "Steam installation not found"; +} diff --git a/src/arma3-unix-launcher-library/exceptions/steam_install_not_found.hpp b/src/arma3-unix-launcher-library/exceptions/steam_install_not_found.hpp new file mode 100644 index 0000000..2cc9025 --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/steam_install_not_found.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +class SteamInstallNotFoundException : public std::exception +{ + public: + const char *what() const noexcept override; +}; diff --git a/src/arma3-unix-launcher-library/exceptions/steam_workshop_directory_not_found.cpp b/src/arma3-unix-launcher-library/exceptions/steam_workshop_directory_not_found.cpp new file mode 100644 index 0000000..a5b8326 --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/steam_workshop_directory_not_found.cpp @@ -0,0 +1,11 @@ +#include "steam_workshop_directory_not_found.hpp" + +SteamWorkshopDirectoryNotFoundException::SteamWorkshopDirectoryNotFoundException(std::string const &appid) : + message("Steam Workshop directory not found for appid: " + appid) +{ +} + +const char *SteamWorkshopDirectoryNotFoundException::SteamWorkshopDirectoryNotFoundException::what() const noexcept +{ + return message.c_str(); +} diff --git a/src/arma3-unix-launcher-library/exceptions/steam_workshop_directory_not_found.hpp b/src/arma3-unix-launcher-library/exceptions/steam_workshop_directory_not_found.hpp new file mode 100644 index 0000000..0bb9686 --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/steam_workshop_directory_not_found.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class SteamWorkshopDirectoryNotFoundException : public std::exception +{ + public: + explicit SteamWorkshopDirectoryNotFoundException(std::string const &appid); + + const char *what() const noexcept override; + + private: + std::string message; +}; diff --git a/src/arma3-unix-launcher-library/exceptions/syntax_error.cpp b/src/arma3-unix-launcher-library/exceptions/syntax_error.cpp new file mode 100644 index 0000000..9aff09a --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/syntax_error.cpp @@ -0,0 +1,10 @@ +#include "syntax_error.hpp" + +SyntaxErrorException::SyntaxErrorException(std::string const &error): message("Syntax error: " + error) +{ +} + +const char *SyntaxErrorException::what() const noexcept +{ + return message.c_str(); +} diff --git a/src/arma3-unix-launcher-library/exceptions/syntax_error.hpp b/src/arma3-unix-launcher-library/exceptions/syntax_error.hpp new file mode 100644 index 0000000..0eed8b9 --- /dev/null +++ b/src/arma3-unix-launcher-library/exceptions/syntax_error.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class SyntaxErrorException : public std::exception +{ + public: + explicit SyntaxErrorException(std::string const &error); + + const char *what() const noexcept override; + + private: + std::string message; +}; diff --git a/src/arma3-unix-launcher-library/filesystem_utils.cpp b/src/arma3-unix-launcher-library/filesystem_utils.cpp new file mode 100644 index 0000000..c4d40b8 --- /dev/null +++ b/src/arma3-unix-launcher-library/filesystem_utils.cpp @@ -0,0 +1,34 @@ +#include "filesystem_utils.hpp" + +#include "string_utils.hpp" + +namespace FilesystemUtils +{ + bool CreateDirectories(std::filesystem::path const &path) + { + return std::filesystem::create_directories(path); + } + + bool Exists(std::filesystem::path const &path) + { + return std::filesystem::exists(path); + } + + bool IsDirectory(std::filesystem::path const &path) + { + return std::filesystem::is_directory(path); + } + + std::vector Ls(std::filesystem::path const &path, bool set_lowercase) + { + std::vector ret; + for (auto const &entity : std::filesystem::directory_iterator(path)) + { + if (set_lowercase) + ret.emplace_back(StringUtils::Lowercase(entity.path().filename())); + else + ret.emplace_back(entity.path().filename()); + } + return ret; + } +} diff --git a/src/arma3-unix-launcher-library/filesystem_utils.hpp b/src/arma3-unix-launcher-library/filesystem_utils.hpp new file mode 100644 index 0000000..76bf284 --- /dev/null +++ b/src/arma3-unix-launcher-library/filesystem_utils.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include +#include +#include + +namespace FilesystemUtils +{ + bool CreateDirectories(std::filesystem::path const &path); + bool Exists(std::filesystem::path const &path); + bool IsDirectory(std::filesystem::path const &path); + std::vector Ls(std::filesystem::path const &path, bool set_lowercase = false); +} diff --git a/src/arma3-unix-launcher-library/mod.cpp b/src/arma3-unix-launcher-library/mod.cpp new file mode 100644 index 0000000..3ac1a3a --- /dev/null +++ b/src/arma3-unix-launcher-library/mod.cpp @@ -0,0 +1,70 @@ +#include "mod.hpp" + +#include +#include +#include +#include + +#include "exceptions/directory_not_found.hpp" + +#include "filesystem_utils.hpp" +#include "std_utils.hpp" +#include "string_utils.hpp" + +namespace fs = FilesystemUtils; + +Mod::Mod(std::filesystem::path const &path) : path_(path) +{ + if (!StdUtils::Contains(fs::Ls(path, true), "addons")) + throw DirectoryNotFoundException(path / "addons"); + + LoadAllCPP(); + if (!StdUtils::ContainsKey(KeyValue, "publishedid") || KeyValue["publishedid"] == "0") + KeyValue["publishedid"] = path.filename(); +} + +std::string Mod::GetName() +{ + return GetValueOrReturnDefault("name", "dir", "tooltip", "publishedid", path_.filename()); +} + +void Mod::LoadAllCPP() +{ + for (auto const &cppfile : fs::Ls(path_)) + if (StringUtils::EndsWith(cppfile, ".cpp")) + LoadFromText(StdUtils::FileReadAllText(path_ / cppfile), true); +} + +void Mod::LoadFromText(std::string const &text, bool append) +{ + if (!append) + KeyValue.clear(); + ParseCPP(RemoveWhitespacesAndComments(text)); +} + +std::string Mod::RemoveWhitespacesAndComments(std::string const &text) +{ + std::regex remove_whitespaces(R"(\s+(?=([^"]*"[^"]*")*[^"]*$))"); + std::regex remove_comments(R"(\s+(?=([^"]*"[^"]*")*[^"]*$))"); + return std::regex_replace(std::regex_replace(text, remove_comments, ""), remove_whitespaces, ""); +} + +void Mod::ParseCPP(std::string const &text) +{ + std::vector lines = StringUtils::Split(text, ";"); + for (auto const &line : StringUtils::Split(text, ";")) + { + size_t split_place = line.find('='); + if (split_place == std::string_view::npos) + continue; + + size_t value_start = split_place + 1; + size_t value_end = line.length(); + if (line[value_start] == '"') // Remove quotes + value_start++; + if (line[value_end - 1] == '"') + value_end--; + + KeyValue[std::string(line.substr(0, split_place))] = std::string(line.substr(value_start, value_end - value_start)); + } +} diff --git a/src/arma3-unix-launcher-library/mod.hpp b/src/arma3-unix-launcher-library/mod.hpp new file mode 100644 index 0000000..a32762b --- /dev/null +++ b/src/arma3-unix-launcher-library/mod.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include + +#include + +class Mod +{ + public: + Mod(std::filesystem::path const &path); + + std::filesystem::path path_; + std::map KeyValue; + + std::string GetName(); + void LoadAllCPP(); + void LoadFromText(std::string const &text, bool append = false); + + std::string GetValueOrReturnDefault(std::string default_value) const + { + return default_value; + } + + template + std::string GetValueOrReturnDefault(std::string key, Args... keys) const + { + auto iterator = KeyValue.find(key); + if (iterator != KeyValue.end()) + return iterator->second; + return GetValueOrReturnDefault(keys...); + } + + bool operator==(Mod const &other) const + { + return other.path_ == path_ && other.KeyValue == KeyValue; + } + + operator std::string() const + { + std::string out_string = "Path: " + path_.string() + "\n"; + for (const auto &[key, value] : KeyValue) + out_string += "Key: " + key + " Value: " + value + "\n"; + return out_string; + } + + friend ::std::ostream &operator<<(::std::ostream &os, Mod const &mod) + { + return os << std::string(mod); + } + + private: + std::string RemoveWhitespacesAndComments(const std::string &text); + void ParseCPP(const std::string &text); +}; diff --git a/src/arma3-unix-launcher-library/std_utils.cpp b/src/arma3-unix-launcher-library/std_utils.cpp new file mode 100644 index 0000000..2821655 --- /dev/null +++ b/src/arma3-unix-launcher-library/std_utils.cpp @@ -0,0 +1,155 @@ +#include "std_utils.hpp" + +#include +#include +#include + +#include + +namespace StdUtils +{ + bool CreateFile(std::filesystem::path const &path) + { + auto result = open(path.c_str(), O_CREAT | O_WRONLY, 0644); + if (result == -1) + return false; + close(result); + return true; + } + + std::pair ExecuteCommand(std::string_view const command) + { + std::array buffer{}; + int exit_code = -1; + std::string output; + + { + // local scope kicks off pclose before returning exit_code + auto deleter = [&exit_code](std::FILE * ptr) + { + exit_code = pclose(ptr); + }; + std::unique_ptr pipe(popen(command.data(), "r"), deleter); + + if (!pipe) + throw std::runtime_error("popen() failed!"); + + while (std::fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) + output += buffer.data(); + } + + return {exit_code, output}; + } + + + std::string FileReadAllText(std::filesystem::path const &path) + { + std::ifstream file(path); + std::stringstream str; + str << file.rdbuf(); + return str.str(); + } + + void FileWriteAllText(std::filesystem::path const &path, std::string const &text) + { + if (!exists(path.parent_path())) + throw std::filesystem::filesystem_error("Parent dir does not exist", {}); + std::ofstream(path) << text; + } + + pid_t IsProcessRunning(std::string const &name, bool case_insensitive) + { + #ifdef __linux + for (auto const &entity : std::filesystem::directory_iterator("/proc")) + { + try + { + if (entity.is_symlink() || !entity.is_directory() || !exists(entity.path() / "exe")) + continue; + + std::filesystem::path exe_path = read_symlink(entity.path() / "exe"); + + using StringUtils::Lowercase; + using StringUtils::trim; + + if (case_insensitive) + { + if (Lowercase(exe_path.filename()) == Lowercase(name)) + return std::stoi(entity.path().filename()); + else if (exe_path.filename() == "wine64-preloader" + && trim(Lowercase(FileReadAllText(entity.path() / "comm"))) == Lowercase(name)) + return std::stoi(entity.path().filename()); + } + else + { + if (exe_path.filename() == name) + return std::stoi(entity.path().filename()); + else if (exe_path.filename() == "wine64-preloader" && trim(FileReadAllText(entity.path() / "comm")) == name) + return std::stoi(entity.path().filename()); + } + } + catch (std::filesystem::filesystem_error const &) + { + // most likely ACCCESS DENIED to other users' processes + } + } + + return -1; + #else + auto processes = ExecuteCommand("ps -eo pid=,ucomm="); + if (processes.first != 0) + return -1; + + auto name_looking_for = name; + if (StringUtils::EndsWith(name_looking_for, ".app")) + name_looking_for = name_looking_for.substr(0, name_looking_for.size() - 4); + if (case_insensitive) + name_looking_for = StringUtils::Lowercase(name_looking_for); + + for (auto const &line : StringUtils::Split(processes.second, "\n")) // line: 525 ArmA3.App + { + auto trimmed = StringUtils::trim(line); + auto space_index = trimmed.find(' '); + if (space_index == trimmed.npos) + { + fmt::print("Didnt find space in line \"{}\" from ps output\n", trimmed); + continue; + } + + auto proc_id = trimmed.substr(0, space_index); + auto proc_name = trimmed.substr(space_index + 1); + + std::string process_name(proc_name); + if (case_insensitive) + process_name = StringUtils::Lowercase(std::string(proc_name)); + if (name_looking_for == process_name) + return std::stoi(std::string(proc_id)); + } + return -1; + #endif + } + + void StartBackgroundProcess(std::string const &command) + { + auto pid = fork(); + if (pid < 0) + throw std::runtime_error("cannot fork to start background process"); + if (!pid) + { + setsid(); + system(command.c_str()); + exit(0); + } + } + + std::filesystem::path GetConfigFilePath(std::filesystem::path const &config_filename) + { + std::filesystem::path config_directory = fmt::format("{}/.config/a3unixlauncher", getenv("HOME")); + + auto xdg_config_home = getenv("XDG_CONFIG_HOME"); + if (xdg_config_home) + config_directory = fmt::format("{}/a3unixlauncher", xdg_config_home); + + return config_directory / config_filename; + } +} diff --git a/src/arma3-unix-launcher-library/std_utils.hpp b/src/arma3-unix-launcher-library/std_utils.hpp new file mode 100644 index 0000000..321039f --- /dev/null +++ b/src/arma3-unix-launcher-library/std_utils.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "exceptions/path_no_access.hpp" + +#include "string_utils.hpp" + +namespace StdUtils +{ + template> + bool Contains(container const &cnt, T const &t) + { + return std::find(cnt.begin(), cnt.end(), t) != cnt.end(); + } + + template + bool ContainsKey(std::map const &map, T const &key) + { + return map.find(key) != map.end(); + } + + template + bool ContainsKey(std::map const &map, char const *const key) + { + return map.find(std::string(key)) != map.end(); + } + + bool CreateFile(std::filesystem::path const &path); + std::pair ExecuteCommand(std::string_view const command); + std::string FileReadAllText(std::filesystem::path const &path); + void FileWriteAllText(std::filesystem::path const &path, std::string const &text); + pid_t IsProcessRunning(std::string const &name, bool case_insensitive = false); + void StartBackgroundProcess(std::string const &command); + std::filesystem::path GetConfigFilePath(std::filesystem::path const &config_filename); +} diff --git a/src/arma3-unix-launcher-library/steam.cpp b/src/arma3-unix-launcher-library/steam.cpp new file mode 100644 index 0000000..26c2eda --- /dev/null +++ b/src/arma3-unix-launcher-library/steam.cpp @@ -0,0 +1,73 @@ +#include "steam.hpp" + +#include +#include + +#include "filesystem_utils.hpp" +#include "std_utils.hpp" +#include "string_utils.hpp" +#include "vdf.hpp" + +#include "exceptions/steam_install_not_found.hpp" +#include "exceptions/steam_workshop_directory_not_found.hpp" + +using namespace StringUtils; + +using std::filesystem::exists; +using std::filesystem::path; + +namespace fs = FilesystemUtils; + +Steam::Steam(std::vector search_paths) +{ + steam_path_ = ""; + for (auto const &search_path : search_paths) + { + path replace_var = Replace(search_path.c_str(), "$HOME", getenv("HOME")); + std::string final_path = replace_var / config_path_; + if (fs::Exists(final_path)) + { + steam_path_ = replace_var; + break; + } + } + if (steam_path_.empty()) + throw SteamInstallNotFoundException(); +} + +path const &Steam::GetSteamPath() const noexcept +{ + return steam_path_; +} + +std::vector Steam::GetInstallPaths() const +{ + std::vector ret; + ret.emplace_back(steam_path_); + + VDF vdf; + vdf.LoadFromText(StdUtils::FileReadAllText(steam_path_ / config_path_)); + + for (auto const &key : vdf.GetValuesWithFilter("BaseInstallFolder")) + ret.emplace_back(key); + + return ret; +} + +path Steam::GetGamePathFromInstallPath(path const &install_path, std::string const &appid) const +{ + std::filesystem::path manifest_file = install_path / "steamapps" / ("appmanifest_" + appid + ".acf"); + + VDF vdf; + vdf.LoadFromText(StdUtils::FileReadAllText(manifest_file)); + return install_path / "steamapps/common" / vdf.KeyValue["AppState/installdir"]; +} + +path Steam::GetWorkshopPath(path const &install_path, std::string const &appid) const +{ + path proposed_path = install_path / "steamapps/workshop/content" / appid; + if (fs::Exists(proposed_path)) + return proposed_path; + + throw SteamWorkshopDirectoryNotFoundException(appid); +} diff --git a/src/arma3-unix-launcher-library/steam.hpp b/src/arma3-unix-launcher-library/steam.hpp new file mode 100644 index 0000000..d30b7f6 --- /dev/null +++ b/src/arma3-unix-launcher-library/steam.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +class Steam +{ + public: + Steam(std::vector search_paths = {"$HOME/.local/share/Steam", "$HOME/.steam/steam", "$HOME/Library/Application Support/Steam"}); + + std::filesystem::path const &GetSteamPath() const noexcept; + std::vector GetInstallPaths() const; + std::filesystem::path GetGamePathFromInstallPath(std::filesystem::path const &install_path, + std::string const &appid) const; + std::filesystem::path GetWorkshopPath(std::filesystem::path const &install_path, std::string const &appid) const; + + private: + std::filesystem::path steam_path_; + std::filesystem::path config_path_ = "config/config.vdf"; +}; diff --git a/src/arma3-unix-launcher-library/string_utils.cpp b/src/arma3-unix-launcher-library/string_utils.cpp new file mode 100644 index 0000000..4281219 --- /dev/null +++ b/src/arma3-unix-launcher-library/string_utils.cpp @@ -0,0 +1,116 @@ +#include "string_utils.hpp" + +#include +#include + +#include + +namespace StringUtils +{ + size_t find_last_nth(std::string const &text, char const c, int count = 1) + { + ssize_t found_chars = 0; + ssize_t found_i = 0; + for (ssize_t i = text.size() - 1; i >= 0; i--) + { + if (text[i] == c) + { + found_chars++; + found_i = i; + } + if (found_chars == count) + return found_i; + } + if (found_chars > 0) + return found_i; + return std::string::npos; + } + + std::string_view RemoveElementsFromPath(std::string const &text, bool remove_slash, int count) + { + if (text.length() == 0) + return std::string_view(text.c_str(), text.size()); + + size_t pos = find_last_nth(text, '/', count); + if (pos == std::string::npos) + return std::string_view(text.c_str(), text.size()); + if (!remove_slash) + pos++; + return std::string_view(text.c_str(), pos); + } + + std::string Replace(std::string text, std::string const &from, std::string const &to) + { + if (from.empty()) + return text; + + size_t start_pos = 0; + while (start_pos != std::string::npos) + { + start_pos = text.find(from, start_pos); + if (start_pos == std::string::npos) + return text; + text.replace(start_pos, from.length(), to); + //move start_pos forward so it doesn't replace the same string over and over + start_pos += to.length() - from.length() + 2; + } + return text; + } + + bool EndsWith(std::string const &text, std::string const &find) + { + if (find.size() > text.size()) + return false; + return find == std::string_view(text.c_str() + text.size() - find.size(), find.size()); + } + + bool StartsWith(std::string const &text, std::string const &find) + { + if (find.size() > text.size()) + return false; + return find == std::string_view(text.c_str(), find.size()); + } + + std::vector Split(std::string const &text_to_split, std::string const &delimiters) + { + std::vector ret; + + size_t start_trim_size = text_to_split.find_first_not_of(delimiters); + size_t end_trim_size = text_to_split.find_last_not_of(delimiters); + if (start_trim_size == std::string::npos || end_trim_size == std::string::npos) + return ret; + std::string_view trimmed(text_to_split.c_str() + start_trim_size, end_trim_size - start_trim_size + 1); + + size_t start_pos = 0; + while (true) + { + start_pos = trimmed.find_first_not_of(delimiters, start_pos); + auto end_pos = trimmed.find_first_of(delimiters, start_pos); + if (end_pos == std::string::npos) + { + ret.push_back(trimmed.substr(start_pos, trimmed.size() - start_pos)); + break; + } + ret.push_back(trimmed.substr(start_pos, end_pos - start_pos)); + start_pos = end_pos; + } + + return ret; + } + + std::string Lowercase(std::string text) + { + std::transform(text.begin(), text.end(), text.begin(), ::tolower); + return text; + } + + std::filesystem::path ToWindowsPath(std::filesystem::path const &path, char const drive_letter) + { + if (path.empty()) + return path; + std::string path_str = Replace(path.c_str(), "/", "\\"); + if (path.is_absolute()) + return fmt::format("{}:{}", drive_letter, path_str); + return path_str; + } +} diff --git a/src/arma3-unix-launcher-library/string_utils.hpp b/src/arma3-unix-launcher-library/string_utils.hpp new file mode 100644 index 0000000..19aa42a --- /dev/null +++ b/src/arma3-unix-launcher-library/string_utils.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace StringUtils +{ + std::string_view RemoveElementsFromPath(std::string const &text, bool remove_slash = true, int count = 1); + std::string Replace(std::string text, std::string const &from, std::string const &to); + + bool EndsWith(std::string const &text, std::string const &find); + bool StartsWith(std::string const &text, std::string const &find); + + std::vector Split(std::string const &text_to_split, std::string const &delimiters); + + std::string Lowercase(std::string text); + + std::filesystem::path ToWindowsPath(std::filesystem::path const &path, char const drive_letter = 'C'); + + constexpr std::string_view trim_left(std::string_view text, + std::string_view const to_trim = std::string_view(" \n\r\t\0", 5)) + { + text.remove_prefix(std::min(text.find_first_not_of(to_trim), text.size())); + return text; + } + + constexpr std::string_view trim_right(std::string_view text, + std::string_view const to_trim = std::string_view(" \n\r\t\0", 5)) + { + size_t const end_pos = text.find_last_not_of(to_trim); + if (end_pos != std::string_view::npos) + text.remove_suffix(text.length() - end_pos - 1); + + return text; + } + + constexpr std::string_view trim(std::string_view text, + std::string_view const to_trim = std::string_view(" \n\r\t\0", 5)) + { + return trim_left(trim_right(text, to_trim)); + } +} diff --git a/src/arma3-unix-launcher-library/vdf.cpp b/src/arma3-unix-launcher-library/vdf.cpp new file mode 100644 index 0000000..b984fae --- /dev/null +++ b/src/arma3-unix-launcher-library/vdf.cpp @@ -0,0 +1,133 @@ +#include "vdf.hpp" + +#include "exceptions/syntax_error.hpp" + +#include "std_utils.hpp" + +#include + +std::vector VDF::GetValuesWithFilter(std::string_view const filter) +{ + std::vector result; + for (auto const &[key, value] : KeyValue) + { + if (key.find(filter) != std::string::npos) + result.emplace_back(value); + } + return result; +} + +void VDF::LoadFromText(std::string_view const text, bool append) +{ + if (!append) + KeyValue.clear(); + ParseVDF(RemoveWhitespaces(text)); +} + +void VDF::AddKeyValuePair() +{ + std::string key_path; + for (auto const &str : hierarchy_) + key_path += str + "/"; + key_path += key_; + KeyValue[key_path] = value_; + key_ = ""; + value_ = ""; +} + +std::string VDF::RemoveWhitespaces(std::string_view const text) +{ + /* + * This crashes, is the string too long? + * std::regex regex("\\s+(?=([^\"]*\"[^\"]*\")*[^\"]*$)"); + * std::string text_without_whitespaces = std::regex_replace(text, regex, ""); + * return text_without_whitespaces; + * + * Anyways, here is very dumb implementation + */ + std::string ret; + ret.reserve(text.length()); + bool in_quotes = false; + for (auto c : text) + { + if (c == '"') + in_quotes = !in_quotes; + if (!std::isspace(c) || in_quotes) + ret += c; + } + return ret; +} + +void VDF::ParseVDF(std::string const &text) +{ + state_ = VDFState::LookingForKey; + key_ = ""; + value_ = ""; + hierarchy_.clear(); + for (auto c : text) + ProcessChar(c); + + if (!hierarchy_.empty()) + throw SyntaxErrorException("Unclosed brackets in VDF"); +} + +void VDF::ProcessChar(char c) +{ + if (state_ == VDFState::LookingForKey) + LookForKey(c); + else if (state_ == VDFState::LookingForValue) + LookForValue(c); + else if (state_ == VDFState::ReadingKey) + ReadKey(c); + else if (state_ == VDFState::ReadingValue) + ReadValue(c); +} + +void VDF::LookForKey(char c) +{ + if (c == '"') + state_ = VDFState::ReadingKey; + else if (c == '}') + hierarchy_.pop_back(); + else + throw SyntaxErrorException("VDF: Quote or bracket expected"); +} + +void VDF::LookForValue(char c) +{ + if (c == '"') + state_ = VDFState::ReadingValue; + else if (c == '{') + { + hierarchy_.emplace_back(key_); + key_ = ""; + state_ = VDFState::LookingForKey; + } + else if (c == '}') + { + hierarchy_.pop_back(); + key_ = ""; + state_ = VDFState::LookingForKey; + } + else + throw SyntaxErrorException("VDF: Quote or bracket expected"); +} + +void VDF::ReadKey(char c) +{ + if (c != '"') + key_ += c; + else + state_ = VDFState::LookingForValue; +} + +void VDF::ReadValue(char c) +{ + if (c != '"') + value_ += c; + else + { + state_ = VDFState::LookingForKey; + AddKeyValuePair(); + } +} diff --git a/src/arma3-unix-launcher-library/vdf.hpp b/src/arma3-unix-launcher-library/vdf.hpp new file mode 100644 index 0000000..8756c7f --- /dev/null +++ b/src/arma3-unix-launcher-library/vdf.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include + +class VDF +{ + public: + VDF() noexcept : state_(VDFState::LookingForKey) {}; + + void LoadFromText(std::string_view const text, bool append = false); + std::vector GetValuesWithFilter(std::string_view const filter); + + std::map KeyValue; + + private: + void AddKeyValuePair(); + std::string RemoveWhitespaces(std::string_view const text); + void ParseVDF(std::string const &text); + void ProcessChar(char c); + void LookForKey(char c); + void LookForValue(char c); + void ReadKey(char c); + void ReadValue(char c); + + enum class VDFState + { + LookingForKey, + LookingForValue, + ReadingKey, + ReadingValue, + QuoteOrBracketExpectedError, + MissingBracketAtEndError + }; + VDFState state_; + std::string key_; + std::string value_; + std::vector hierarchy_; +}; diff --git a/src/arma3-unix-launcher/CMakeLists.txt b/src/arma3-unix-launcher/CMakeLists.txt new file mode 100644 index 0000000..f1a4b36 --- /dev/null +++ b/src/arma3-unix-launcher/CMakeLists.txt @@ -0,0 +1,37 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +find_package(Qt5Widgets CONFIG REQUIRED) +find_package(Qt5Svg CONFIG REQUIRED) + +file(GLOB SOURCES *.cpp *.h *.hpp *.qrc *.ui) + +add_executable(arma3-unix-launcher MACOSX_BUNDLE ${SOURCES}) +target_link_libraries(arma3-unix-launcher PRIVATE + arma3-unix-launcher-library + argparse::argparse + fmt::fmt + nlohmann::json + Qt5::Widgets + Qt5::Svg) +#install(TARGETS arma3-unix-launcher DESTINATION bin) +install(TARGETS arma3-unix-launcher + BUNDLE DESTINATION . COMPONENT Runtime + RUNTIME DESTINATION bin COMPONENT Runtime) + +if (APPLE) + find_package(Qt5Widgets CONFIG REQUIRED) + get_target_property(WIDGETS_LOCATION Qt5::Widgets IMPORTED_LOCATION) + get_filename_component(QT_BIN_DIR "${WIDGETS_LOCATION}" DIRECTORY) + find_program(MACDEPLOYQT_EXECUTABLE macdeployqt HINTS "${QT_BIN_DIR}") + + add_custom_command(TARGET arma3-unix-launcher POST_BUILD + COMMAND "${MACDEPLOYQT_EXECUTABLE}" + "$/../.." + -always-overwrite + COMMENT "Running macdeployqt...") + + set(CPACK_GENERATOR "DragNDrop") + include(CPack) +endif() diff --git a/src/arma3-unix-launcher/arma_path_chooser_dialog.cpp b/src/arma3-unix-launcher/arma_path_chooser_dialog.cpp new file mode 100644 index 0000000..97d7a0d --- /dev/null +++ b/src/arma3-unix-launcher/arma_path_chooser_dialog.cpp @@ -0,0 +1,138 @@ +#include "arma_path_chooser_dialog.h" +#include "ui_arma_path_chooser_dialog.h" + +#include +#include +#include +#include + +#include +#include + +#include "exceptions/file_not_found.hpp" + +#include "arma3client.hpp" + +ArmaPathChooserDialog::ArmaPathChooserDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::ArmaPathChooserDialog) +{ + ui->setupUi(this); + ui->horizontalLayout_2->setAlignment(Qt::AlignTop); + ui->horizontalLayout->setAlignment(Qt::AlignBottom); + ui->verticalLayout->setAlignment(Qt::AlignTop); + + this->setMaximumSize(1024000, 1); + this->setMinimumSize(450, 106); + + button_ok_ = ui->buttonBox->button(ui->buttonBox->Ok); + + QIcon icon_ok = QIcon(":/icons/open-iconic/check.svg"); + pixmap_ok_ = icon_ok.pixmap(QSize(16, 16)); + + QIcon icon_error = QIcon(":/icons/open-iconic/x.svg"); + pixmap_error_ = icon_error.pixmap(QSize(16, 16)); + + on_text_arma_path_textChanged("/invalid-path"); + on_text_workshop_path_textChanged("/invalid-path"); +} + +ArmaPathChooserDialog::~ArmaPathChooserDialog() +{ + delete ui; +} + +void ArmaPathChooserDialog::on_text_arma_path_textChanged(QString const &arg1) +{ + if (!is_arma_path_valid(arg1)) + { + button_ok_->setEnabled(false); + ui->icon_arma_path->setPixmap(pixmap_error_); + return; + } + + button_ok_->setEnabled(true); + ui->icon_arma_path->setPixmap(pixmap_ok_); + QString workshop_path = ui->text_arma_path->text() + "/../../workshop/content/107410"; + try + { + std::filesystem::path canonical_path = std::filesystem::canonical(workshop_path.toStdString()); + ui->text_workshop_path->setText(QString::fromStdString(canonical_path.string())); + } + catch (std::filesystem::filesystem_error &exception) + { + fmt::print("{}\n", exception.what()); + } +} + +void ArmaPathChooserDialog::on_text_workshop_path_textChanged(QString const &arg1) +{ + if (!is_workshop_path_valid(arg1)) + ui->icon_workshop_path->setPixmap(pixmap_error_); + else + ui->icon_workshop_path->setPixmap(pixmap_ok_); +} + +void ArmaPathChooserDialog::on_button_browse_arma_path_clicked() +{ + using namespace std::filesystem; + + auto open_executable_dialog = get_open_dialog("Select ArmA 3 executable"); + open_executable_dialog->setFileMode(QFileDialog::ExistingFile); + int result = open_executable_dialog->exec(); + if (!result) + return; + + auto executable_path = open_executable_dialog->selectedFiles()[0]; + path parent_path = path(executable_path.toStdString()).parent_path(); + ui->text_arma_path->setText(parent_path.c_str()); +} + +void ArmaPathChooserDialog::on_button_browse_workshop_path_clicked() +{ + auto open_dir_dialog = get_open_dialog("Select ArmA 3 workshop path"); + open_dir_dialog->setOption(QFileDialog::ShowDirsOnly); + int result = open_dir_dialog->exec(); + if (!result) + return; + + auto workshop_dir = open_dir_dialog->selectedFiles()[0]; + ui->text_workshop_path->setText(workshop_dir); +} + +std::unique_ptr ArmaPathChooserDialog::get_open_dialog(QString const &title) +{ + auto dialog = std::make_unique(); + dialog->setFilter(QDir::AllDirs | QDir::AllEntries | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot); + dialog->setWindowTitle(title); + dialog->setViewMode(QFileDialog::Detail); + dialog->setOption(QFileDialog::DontResolveSymlinks, false); + dialog->setDirectory(QDir::homePath()); + return dialog; +} + +bool ArmaPathChooserDialog::is_arma_path_valid(QString const &arg1) +{ + try + { + ARMA3::Client client(arg1.toStdString(), ""); + return true; + } + catch (...) + { + return false; + } +} + +bool ArmaPathChooserDialog::is_workshop_path_valid(QString const &arg1) +{ + using namespace std::filesystem; + std::error_code ec; + return is_directory(arg1.toStdString(), ec); +} + +void ArmaPathChooserDialog::on_buttonBox_accepted() +{ + arma_path_ = ui->text_arma_path->text().toStdString(); + workshop_path_ = ui->text_workshop_path->text().toStdString(); +} diff --git a/src/arma3-unix-launcher/arma_path_chooser_dialog.h b/src/arma3-unix-launcher/arma_path_chooser_dialog.h new file mode 100644 index 0000000..0adcbb6 --- /dev/null +++ b/src/arma3-unix-launcher/arma_path_chooser_dialog.h @@ -0,0 +1,47 @@ +#ifndef ARMA_PATH_CHOOSER_DIALOG_H +#define ARMA_PATH_CHOOSER_DIALOG_H + +#include +#include + +#include + +namespace Ui +{ + class ArmaPathChooserDialog; +} + +class ArmaPathChooserDialog : public QDialog +{ + Q_OBJECT + + public: + explicit ArmaPathChooserDialog(QWidget *parent = nullptr); + ~ArmaPathChooserDialog(); + + std::filesystem::path arma_path_; + std::filesystem::path workshop_path_; + + private slots: + void on_text_arma_path_textChanged(QString const &arg1); + void on_text_workshop_path_textChanged(QString const &arg1); + + void on_button_browse_arma_path_clicked(); + void on_button_browse_workshop_path_clicked(); + + void on_buttonBox_accepted(); + +private: + Ui::ArmaPathChooserDialog *ui; + QPixmap pixmap_error_; + QPixmap pixmap_ok_; + + QPushButton *button_ok_; + + std::unique_ptr get_open_dialog(QString const &title); + + bool is_arma_path_valid(QString const &arg1); + bool is_workshop_path_valid(QString const &arg1); +}; + +#endif // ARMA_PATH_CHOOSER_DIALOG_H diff --git a/src/arma3-unix-launcher/arma_path_chooser_dialog.ui b/src/arma3-unix-launcher/arma_path_chooser_dialog.ui new file mode 100644 index 0000000..e9d600e --- /dev/null +++ b/src/arma3-unix-launcher/arma_path_chooser_dialog.ui @@ -0,0 +1,185 @@ + + + ArmaPathChooserDialog + + + + 0 + 0 + 646 + 106 + + + + + 0 + 0 + + + + Choose ARMA 3 and Workshop paths + + + + QLayout::SetDefaultConstraint + + + + + 0 + + + + + + 0 + 0 + + + + + 16 + 16 + + + + + + + :/icons/open-iconic/x.svg + + + + + + + + 92 + 0 + + + + ARMA 3 path + + + + + + + + + + Browse + + + + + + + + + QLayout::SetDefaultConstraint + + + + + + 16 + 16 + + + + + + + :/icons/open-iconic/x.svg + + + + + + + + 92 + 0 + + + + Workshop path + + + + + + + + + + Browse + + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + text_arma_path + button_browse_arma_path + text_workshop_path + button_browse_workshop_path + + + + + + + buttonBox + accepted() + ArmaPathChooserDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ArmaPathChooserDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/arma3-unix-launcher/icons.qrc b/src/arma3-unix-launcher/icons.qrc new file mode 100644 index 0000000..4a7c132 --- /dev/null +++ b/src/arma3-unix-launcher/icons.qrc @@ -0,0 +1,10 @@ + + + open-iconic/check.svg + open-iconic/x.svg + open-iconic/minus.svg + open-iconic/plus.svg + open-iconic/envelope-closed.svg + open-iconic/envelope-open.svg + + diff --git a/src/arma3-unix-launcher/main.cpp b/src/arma3-unix-launcher/main.cpp new file mode 100644 index 0000000..f548cc7 --- /dev/null +++ b/src/arma3-unix-launcher/main.cpp @@ -0,0 +1,199 @@ +#include "arma_path_chooser_dialog.h" +#include "mainwindow.h" +#include + +#include +#include +#include + +#include "arma3client.hpp" +#include "filesystem_utils.hpp" +#include "steam.hpp" +#include "std_utils.hpp" + +#include "exceptions/file_not_found.hpp" + +#include "settings.hpp" + +std::string read_argument(std::string const &argument_name, argparse::ArgumentParser &parser) +{ + try + { + return parser.get(argument_name); + } + catch (std::exception const &) + { + return ""; + } +} + +void start_arma(std::filesystem::path const &preset_to_run, std::string const &arguments, + std::filesystem::path const &config_file, ARMA3::Client &client) +{ + auto get_valid_path = [](std::vector const & paths) + { + for (auto const &path : paths) + if (FilesystemUtils::Exists(path)) + return std::filesystem::path(path); + return std::filesystem::path(); + }; + + std::filesystem::path const config_dir = config_file.parent_path(); + std::filesystem::path const filename_with_extension = preset_to_run.string() + ".a3ulml"; + auto valid_path = get_valid_path({preset_to_run, config_dir / preset_to_run, config_dir / filename_with_extension}); + if (valid_path.empty()) + throw FileNotFoundException(preset_to_run); + + nlohmann::json preset = nlohmann::json::parse(StdUtils::FileReadAllText(valid_path)); + auto const &mods = preset.at("mods"); + + std::vector custom_mods; + std::vector workshop_mods; + + for (auto const &mod : mods["custom"]) + if (mod["enabled"]) + custom_mods.push_back(StringUtils::Replace(mod["path"], "~arma", client.GetPath())); + for (auto const &mod : mods["workshop"]) + workshop_mods.push_back(mod["id"]); + + fmt::print("Starting Arma with preset {}\n", preset_to_run); + client.CreateArmaCfg(workshop_mods, custom_mods); + client.Start(arguments); +} + +int main(int argc, char *argv[]) +{ + try + { + QApplication a(argc, argv); + argparse::ArgumentParser parser("arma3-unix-launcher"); + + parser.add_argument("-l", "--list-presets").help("list available mod presets").default_value(false).implicit_value( + true); + parser.add_argument("-p", + "--preset-to-run").help("preset to run, launcher will start Arma with given mods and exit").nargs(1); + parser.add_argument("--server-ip").help("server ip to connect to, usable only with --preset-to-run").nargs(1); + parser.add_argument("--server-port").help("server port to connect to, usable only with --preset-to-run").nargs(1); + parser.add_argument("--server-password").help("server pasword to connect to, usable only with --preset-to-run").nargs( + 1); + parser.add_argument("-v", "--verbose").help("verbose mode which enables more logging").default_value( + false).implicit_value(true); + try + { + parser.parse_args(argc, argv); + } + catch (const std::runtime_error &err) + { + fmt::print("{}\n{}\n", err.what(), parser.help().str()); + return 1; + } + + auto config_file = StdUtils::GetConfigFilePath("launcher.conf"); + + if (parser.get("--list-presets")) + { + fmt::print("Available presets:\n"); + for (auto const &file : FilesystemUtils::Ls(config_file.parent_path())) + { + if (file == "launcher.conf") + continue; + std::filesystem::path file_path(file); + fmt::print("{}\n", file_path.stem().string()); + } + return 0; + } + + fmt::print("conf file: {}\n", config_file); + + Settings manager(config_file); + + std::string arma_path, workshop_path; + + try + { + arma_path = manager.settings.at("paths").at("arma"); + workshop_path = manager.settings.at("paths").at("workshop"); + } + catch (std::exception const &ex) + { + fmt::print("cannot read config file: {}\n", ex.what()); + } + + std::unique_ptr client; + if (std::filesystem::exists(arma_path)) + client = std::make_unique(arma_path, workshop_path); + + if (arma_path.empty() || !std::filesystem::exists(arma_path)) + { + Steam steam; + + for (auto const &path : steam.GetInstallPaths()) + { + try + { + fmt::print("Install path: {}\n", path); + arma_path = steam.GetGamePathFromInstallPath(path, ARMA3::Definitions::app_id); + workshop_path = steam.GetWorkshopPath(path, ARMA3::Definitions::app_id); + client = std::make_unique(arma_path, workshop_path); + break; + } + catch (std::exception const &e) + { + fmt::print("Didn't find game at {}\nError: {}\n", path, e.what()); + } + } + + if (arma_path.empty() || workshop_path.empty() || client == nullptr) + { + ArmaPathChooserDialog apcd; + apcd.exec(); + + if (apcd.result() != QDialog::Accepted) + exit(0); + + fmt::print("Arma3: {}\nWorkshop: {}\n", apcd.arma_path_, apcd.workshop_path_); + arma_path = apcd.arma_path_; + workshop_path = apcd.workshop_path_; + client = std::make_unique(arma_path, workshop_path); + } + } + + manager.settings["paths"]["arma"] = arma_path; + manager.settings["paths"]["workshop"] = workshop_path; + manager.save_settings_to_disk(); + + auto preset_to_run = read_argument("--preset-to-run", parser); + if (!preset_to_run.empty()) + { + std::string arguments = manager.get_launch_parameters(); + + auto ip = read_argument("--server-ip", parser); + auto port = read_argument("--server-port", parser); + auto password = read_argument("--server-password", parser); + if (!ip.empty()) + arguments += fmt::format(" -connect={}", ip); + if (!port.empty()) + arguments += fmt::format(" -port={}", port); + if (!password.empty()) + arguments += fmt::format(" -password={}", password); + + start_arma(preset_to_run, arguments, config_file, *client); + return 0; + } + + MainWindow w(std::move(client), config_file); + w.show(); + + return a.exec(); + } + catch (std::exception const &ex) + { + fmt::print("exception: {}\nshutting down\n", ex.what()); + return 1; + } + catch (...) + { + fmt::print("unknown exception\n"); + return 1; + } +} diff --git a/src/arma3-unix-launcher/mainwindow.cpp b/src/arma3-unix-launcher/mainwindow.cpp new file mode 100644 index 0000000..e499446 --- /dev/null +++ b/src/arma3-unix-launcher/mainwindow.cpp @@ -0,0 +1,551 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" + +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include + +#include "filesystem_utils.hpp" +#include "string_utils.hpp" +#include "ui_mod.hpp" + +#include "std_utils.hpp" + +#include "static_todo.hpp" + +namespace fs = FilesystemUtils; + +MainWindow::MainWindow(std::unique_ptr arma3_client, std::filesystem::path const &config_file_path, + QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow), + client(std::move(arma3_client)), + config_file(config_file_path), + manager(config_file_path) +{ + ui->setupUi(this); + + initialize_table_widget(*ui->table_workshop_mods, {"Enabled", "Name", "Workshop ID"}); + initialize_table_widget(*ui->table_custom_mods, {"Enabled", "Name", "Path"}); + ui->table_custom_mods->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); + + for (auto const &i : client->GetWorkshopMods()) + { + auto is_mod_enabled = [&](std::string const & workshop_id) + { + for (auto const &mod : manager.settings["mods"]["workshop"]) + if (mod["id"] == workshop_id) + return true; + return false; + }; + + auto mod_id = i.GetValueOrReturnDefault("publishedid", "cannot read id"); + add_item(*ui->table_workshop_mods, {is_mod_enabled(mod_id), i.GetValueOrReturnDefault("name", "cannot read name"), + mod_id + }); + } + + for (auto const &i : client->GetHomeMods()) + { + std::string shortname = StringUtils::Replace(i.path_, client->GetPath().string(), "~arma"); + auto is_mod_enabled = [&](std::string const & shortname) + { + for (auto const &mod : manager.settings["mods"]["custom"]) + if (mod["path"] == shortname && mod["enabled"] == true) + return true; + return false; + }; + add_item(*ui->table_custom_mods, {is_mod_enabled(shortname), + i.GetValueOrReturnDefault("name", "cannot read name"), + shortname + }); + } + + for (auto const &mod : manager.settings["mods"]["custom"]) + { + if (StringUtils::StartsWith(mod["path"], "~arma")) + continue; + try + { + Mod i(mod["path"]); + add_item(*ui->table_custom_mods, {mod["enabled"], i.GetValueOrReturnDefault("name", "cannot read name"), mod["path"]}); + } + catch (std::exception const &ex) + { + fmt::print("{}(): Uncaught exception: {}\n", __FUNCTION__, ex.what()); + } + } + + manager.load_settings_to_ui(ui); + + connect(&arma_status_checker, &QTimer::timeout, this, QOverload<>::of(&MainWindow::check_if_arma_is_running)); + arma_status_checker.start(2000); +} + +MainWindow::~MainWindow() +{ + manager.save_settings_from_ui(ui); + + put_mods_from_ui_to_manager_settings(); + + StdUtils::FileWriteAllText(config_file, manager.settings.dump(4)); + delete ui; +} + +void MainWindow::on_button_start_clicked() +try +{ + manager.save_settings_from_ui(ui); + + auto parameters = manager.settings["parameters"]; + if (parameters["name"].is_string()) + { + std::string profile = parameters["name"]; + if (StringUtils::trim(profile).empty()) + throw std::invalid_argument("Parameters -> Profile cannot be empty"); + } + if (parameters["par"].is_string()) + { + std::string parameter_file = parameters["par"]; + if (StringUtils::trim(parameter_file).empty()) + throw std::invalid_argument("Parameters -> Parameter file cannot be empty"); + } + + std::vector workshop_mod_ids; + for (auto const &mod : get_mods(*ui->table_workshop_mods)) + if (mod.enabled) + workshop_mod_ids.push_back(mod.path_or_workshop_id); + + std::vector custom_mods; + for (auto const &mod : get_mods(*ui->table_custom_mods)) + if (mod.enabled) + custom_mods.push_back(StringUtils::Replace(mod.path_or_workshop_id, "~arma", client->GetPath())); + + client->CreateArmaCfg(workshop_mod_ids, custom_mods); + client->Start(manager.get_launch_parameters()); +} +catch (std::exception const &e) +{ + auto error_message = fmt::format("{}.", e.what()); + QMessageBox(QMessageBox::Icon::Information, "Cannot start game", QString::fromStdString(error_message)).exec(); + return; +} + +void MainWindow::add_item(QTableWidget &table_widget, UiMod const &mod) +{ + int id = table_widget.rowCount(); + table_widget.insertRow(id); + + QWidget *checkbox_widget = new QWidget(); + QHBoxLayout *checkbox_layout = new QHBoxLayout(checkbox_widget); + QCheckBox *checkbox = new QCheckBox(); + + checkbox_layout->addWidget(checkbox); + checkbox_layout->setAlignment(Qt::AlignCenter); + checkbox_layout->setContentsMargins(0, 0, 0, 0); + + checkbox_widget->setLayout(checkbox_layout); + + if (mod.enabled) + checkbox->setCheckState(Qt::Checked); + else + checkbox->setCheckState(Qt::Unchecked); + + table_widget.setCellWidget(id, 0, checkbox_widget); + table_widget.setItem(id, 1, new QTableWidgetItem(mod.name.c_str())); + table_widget.setItem(id, 2, new QTableWidgetItem(mod.path_or_workshop_id.c_str())); + + QObject::connect(checkbox, &QCheckBox::stateChanged, this, &MainWindow::checkbox_changed); + + for (int i = 1; i <= 2; ++i) + { + auto item = table_widget.item(id, i); + item->setFlags(item->flags() ^ Qt::ItemIsEditable); + item->setTextAlignment(Qt::AlignCenter); + } +} + +void MainWindow::initialize_table_widget(QTableWidget &table_widget, QStringList const &column_names) +{ + table_widget.clear(); + table_widget.setHorizontalHeaderLabels(column_names); + table_widget.horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); +} + +std::vector MainWindow::get_mods(QTableWidget const &table_widget) +{ + std::vector mods; + + for (int i = 0; i < table_widget.rowCount(); ++i) + mods.emplace_back(get_mod_from_nth_row(table_widget, i)); + return mods; +} + +UiMod MainWindow::get_mod_from_nth_row(const QTableWidget &table_widget, int row) +{ + auto cell_widget = table_widget.cellWidget(row, 0); + auto checkbox = cell_widget->findChild(); + + bool enabled = checkbox->checkState() == Qt::CheckState::Checked; + std::string name = table_widget.item(row, 1)->text().toStdString(); + std::string path_or_workshop_id = table_widget.item(row, 2)->text().toStdString(); + return {enabled, name, path_or_workshop_id}; +} + +void MainWindow::put_mods_from_ui_to_manager_settings() +{ + auto &custom_mods = manager.settings["mods"]["custom"]; + custom_mods = nlohmann::json::array(); + + for (auto const &mod : get_mods(*ui->table_custom_mods)) + custom_mods.push_back({{"enabled", mod.enabled}, {"path", mod.path_or_workshop_id}}); + + auto &workshop_mods = manager.settings["mods"]["workshop"]; + workshop_mods = nlohmann::json::array(); + + for (auto const &mod : get_mods(*ui->table_workshop_mods)) + if (mod.enabled) + workshop_mods.push_back({{"enabled", true}, {"id", mod.path_or_workshop_id}}); +} + +void MainWindow::checkbox_changed(int) +{ + auto workshop_mods = get_mods(*ui->table_workshop_mods); + auto custom_mods = get_mods(*ui->table_custom_mods); + + int count_workshop = 0; + int count_custom = 0; + + for (auto const &mod : workshop_mods) + if (mod.enabled) + ++count_workshop; + + for (auto const &mod : custom_mods) + if (mod.enabled) + ++count_custom; + + auto text = fmt::format("Selected {} mods ({} from workshop, {} custom)", count_workshop + count_custom, count_workshop, + count_custom); + ui->label_selected_mods->setText(QString::fromStdString(text)); +} + + +std::unique_ptr get_open_dialog(QString const &title) +{ + TODO_BEFORE(04, 2020, "This already exists in arma_path_chooser_dialog.cpp"); + auto dialog = std::make_unique(); + dialog->setFilter(QDir::AllDirs | QDir::AllEntries | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot); + dialog->setWindowTitle(title); + dialog->setFileMode(QFileDialog::FileMode::Directory); + dialog->setViewMode(QFileDialog::ViewMode::Detail); + dialog->setOption(QFileDialog::DontResolveSymlinks, false); + dialog->setOption(QFileDialog::ShowDirsOnly); + dialog->setDirectory(QDir::homePath()); + return dialog; +} + +void MainWindow::on_button_add_custom_mod_clicked() +try +{ + auto open_dir_dialog = get_open_dialog("Select directory with mod to add"); + if (!open_dir_dialog->exec()) + return; + + auto mod_dir_string = open_dir_dialog->selectedFiles()[0].toStdString(); + + using std::filesystem::exists; + if (!exists(mod_dir_string)) + { + auto error_message = fmt::format("{} does not exist.", mod_dir_string); + QMessageBox(QMessageBox::Icon::Information, "Cannot add mod", QString::fromStdString(error_message)).exec(); + return; + } + + auto mod_dir = std::filesystem::canonical(mod_dir_string); + if (mod_dir == client->GetPath()) + { + auto error_message = fmt::format("{} is Arma's main directory.", mod_dir); + QMessageBox(QMessageBox::Icon::Information, "Cannot add mod", QString::fromStdString(error_message)).exec(); + return; + } + + auto dir_listing = fs::Ls(mod_dir, true); + if (!StdUtils::Contains(dir_listing, "addons")) + { + auto error_message = fmt::format("{} does not exist.", mod_dir / "addons"); + QMessageBox(QMessageBox::Icon::Information, "Cannot add mod", QString::fromStdString(error_message)).exec(); + return; + } + + auto &settings_mods_custom = manager.settings["mods"]["custom"]; + auto existing_mod = std::find_if(settings_mods_custom.begin(), + settings_mods_custom.end(), [&mod_dir](nlohmann::json const & item) + { + auto it = item.find("path"); + return it != item.end() && it.value() == mod_dir.string(); + }); + + if (existing_mod != settings_mods_custom.end()) + { + auto error_message = fmt::format("{} already exists.", mod_dir); + QMessageBox(QMessageBox::Icon::Information, "Cannot add mod", QString::fromStdString(error_message)).exec(); + return; + } + + try + { + Mod m(mod_dir); + add_item(*ui->table_custom_mods, UiMod{false, m.GetValueOrReturnDefault("name", "cannot read name"), mod_dir}); + settings_mods_custom.push_back({{"enabled", false}, {"path", mod_dir}}); + } + catch (std::exception const &e) + { + auto error_message = fmt::format("{}.", e.what()); + QMessageBox(QMessageBox::Icon::Information, "Cannot add mod", QString::fromStdString(error_message)).exec(); + return; + } + +} +catch (std::exception const &ex) +{ + fmt::print("{}(): Uncaught exception: {}\n", __FUNCTION__, ex.what()); +} + +void MainWindow::on_button_remove_custom_mod_clicked() +try +{ + auto selected_items = ui->table_custom_mods->selectedItems(); + if (selected_items.size() < 1) + { + QMessageBox(QMessageBox::Icon::Information, "Cannot remove custom mod", "Need to select a mod to remove").exec(); + return; + } + + auto mod = get_mod_from_nth_row(*ui->table_custom_mods, selected_items[0]->row()); + if (StringUtils::StartsWith(mod.path_or_workshop_id, "~arma")) + { + QMessageBox(QMessageBox::Icon::Information, "Cannot remove custom mod", + "Selected mod is in home directory, delete its files manually").exec(); + return; + } + + auto &settings_mods_custom = manager.settings["mods"]["custom"]; + for (auto it = settings_mods_custom.begin(); it < settings_mods_custom.end(); ++it) + { + auto &json_mod = *it; + if (json_mod["path"] == mod.path_or_workshop_id) + settings_mods_custom.erase(it); + } + + ui->table_custom_mods->removeRow(selected_items[0]->row()); +} +catch (std::exception const &ex) +{ + fmt::print("{}(): Uncaught exception: {}\n", __FUNCTION__, ex.what()); +} + +void MainWindow::check_if_arma_is_running() +{ + std::string text = "Status: Arma 3 is not running"; + for (auto const &executable_name : ARMA3::Definitions::executable_names) + if (auto pid = StdUtils::IsProcessRunning(executable_name, true); pid != -1) + text = fmt::format("Status: Arma 3 is running, PID: {}", pid); + + ui->label_arma_status->setText(QString::fromStdString(text)); +} + +void MainWindow::on_checkbox_extra_threads_stateChanged(int arg1) +{ + bool value = arg1 == Qt::CheckState::Checked; + ui->checkbox_extra_threads_file_operations->setEnabled(value); + ui->checkbox_extra_threads_texture_loading->setEnabled(value); + ui->checkbox_extra_threads_geometry_loading->setEnabled(value); +} + +void MainWindow::on_checkbox_cpu_count_stateChanged(int arg1) +{ + ui->spinbox_cpu_count->setEnabled(arg1 == Qt::CheckState::Checked); +} + +void MainWindow::on_checkbox_parameter_file_stateChanged(int arg1) +{ + bool value = arg1 == Qt::CheckState::Checked; + ui->textbox_parameter_file->setEnabled(value); + ui->button_parameter_file_open->setEnabled(value); +} + +void MainWindow::on_checkbox_world_stateChanged(int arg1) +{ + ui->textbox_world->setEnabled(arg1 == Qt::CheckState::Checked); +} + +void MainWindow::on_checkbox_custom_parameters_stateChanged(int arg1) +{ + ui->textbox_custom_parameters->setEnabled(arg1 == Qt::CheckState::Checked); +} + +void MainWindow::on_checkbox_server_address_stateChanged(int arg1) +{ + ui->textbox_server_address->setEnabled(arg1 == Qt::CheckState::Checked); +} + +void MainWindow::on_checkbox_server_port_stateChanged(int arg1) +{ + ui->textbox_server_port->setEnabled(arg1 == Qt::CheckState::Checked); +} + +void MainWindow::on_checkbox_server_password_stateChanged(int arg1) +{ + ui->textbox_server_password->setEnabled(arg1 == Qt::CheckState::Checked); +} + +void MainWindow::on_checkbox_profile_stateChanged(int arg1) +{ + ui->textbox_profile->setEnabled(arg1 == Qt::CheckState::Checked); +} + +void MainWindow::on_button_parameter_file_open_clicked() +{ + auto open_parameter_file_dialog = get_open_dialog("Select parameter file"); + open_parameter_file_dialog->setFileMode(QFileDialog::FileMode::ExistingFile); + open_parameter_file_dialog->setOption(QFileDialog::ShowDirsOnly, false); + if (!open_parameter_file_dialog->exec()) + return; + + auto param_file = open_parameter_file_dialog->selectedFiles()[0].toStdString(); + + using std::filesystem::exists; + if (!exists(param_file)) + { + auto error_message = fmt::format("{} does not exist.", param_file); + QMessageBox(QMessageBox::Icon::Information, "Cannot set parameter file", QString::fromStdString(error_message)).exec(); + return; + } + + ui->textbox_parameter_file->setText(QString::fromStdString(param_file)); +} + +void MainWindow::on_button_mod_preset_open_clicked() +try +{ + auto config_dir = QString::fromStdString(config_file.parent_path().string()); + auto filename = QFileDialog::getOpenFileName(this, tr("Save mod preset"), config_dir, + tr("A3UL mod list | *.a3ulml (*.a3ulml)")); + if (filename.isEmpty()) + return; + + nlohmann::json preset = nlohmann::json::parse(StdUtils::FileReadAllText(filename.toStdString())); + auto &mods = preset.at("mods"); + + auto const &workshop_mods = mods.at("workshop"); + auto workshop_contains_mod = [&](std::string id) + { + for (auto const &mod : workshop_mods) + if (mod.at("id") == id) + return true; + return false; + }; + + for (int row = 0; row < ui->table_workshop_mods->rowCount(); ++row) + { + auto cell_widget = ui->table_workshop_mods->cellWidget(row, 0); + auto checkbox = cell_widget->findChild(); + std::string workshop_id = ui->table_workshop_mods->item(row, 2)->text().toStdString(); + + if (workshop_contains_mod(workshop_id)) + checkbox->setCheckState(Qt::CheckState::Checked); + else + checkbox->setCheckState(Qt::CheckState::Unchecked); + } + + auto &custom_mods = mods.at("custom"); + for (int row = 0; row < ui->table_custom_mods->rowCount(); ++row) + { + auto cell_widget = ui->table_custom_mods->cellWidget(row, 0); + auto checkbox = cell_widget->findChild(); + std::string path = ui->table_custom_mods->item(row, 2)->text().toStdString(); + + auto custom_mod = std::find_if(custom_mods.begin(), custom_mods.end(), [&](nlohmann::json & mod) + { + return mod.at("path") == path; + }); + + if (custom_mod != custom_mods.end()) + (*custom_mod)["done"] = true; + + if (custom_mod != custom_mods.end() && custom_mod->at("enabled")) + checkbox->setCheckState(Qt::CheckState::Checked); + else + checkbox->setCheckState(Qt::CheckState::Unchecked); + } + + std::vector> failed_custom_mods; + for (auto const &mod : custom_mods) + { + try + { + if (mod.contains("done")) + continue; + auto full_path = StringUtils::Replace(mod.at("path"), "~arma", client->GetPath().string()); + Mod m(full_path); + UiMod ui_mod{mod.at("enabled"), m.GetValueOrReturnDefault("name", "cannot read name"), mod.at("path")}; + add_item(*ui->table_custom_mods, ui_mod); + } + catch (std::exception const &e) + { + failed_custom_mods.emplace_back(mod.at("path"), e.what()); + } + } + + if (failed_custom_mods.size() > 0) + { + std::string message = "Mod import ok, failed importing custom mods:\n"; + for (auto const &failed_mod : failed_custom_mods) + message += fmt::format("\nmod: {}, reason: {}", failed_mod.first, failed_mod.second); + QMessageBox(QMessageBox::Icon::Warning, "Imported mod preset, issues found", QString::fromStdString(message)).exec(); + return; + } + put_mods_from_ui_to_manager_settings(); +} +catch (std::exception const &e) +{ + auto error_message = fmt::format("{}.", e.what()); + QMessageBox(QMessageBox::Icon::Critical, "Cannot open mod preset", QString::fromStdString(error_message)).exec(); + return; +} + +void MainWindow::on_button_mod_preset_save_clicked() +try +{ + auto config_dir = QString::fromStdString(config_file.parent_path().string()); + auto filename = QFileDialog::getSaveFileName(this, tr("Save mod preset"), config_dir, + tr("A3UL mod list | *.a3ulml (*.a3ulml)")); + if (filename.isEmpty()) + return; + auto filename_str = filename.toStdString(); + if (!StringUtils::EndsWith(filename_str, ".a3ulml")) + filename_str += ".a3ulml"; + + put_mods_from_ui_to_manager_settings(); + nlohmann::json json; + json["mods"] = manager.settings["mods"]; + StdUtils::FileWriteAllText(filename_str, json.dump(4)); +} +catch (std::exception const &e) +{ + auto error_message = fmt::format("{}.", e.what()); + QMessageBox(QMessageBox::Icon::Critical, "Cannot save mod preset", QString::fromStdString(error_message)).exec(); + return; +} diff --git a/src/arma3-unix-launcher/mainwindow.h b/src/arma3-unix-launcher/mainwindow.h new file mode 100644 index 0000000..85d0491 --- /dev/null +++ b/src/arma3-unix-launcher/mainwindow.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include + +#include "settings.hpp" +#include "ui_mod.hpp" + +namespace Ui +{ + class MainWindow; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + + public: + explicit MainWindow(std::unique_ptr arma3_client, std::filesystem::path const &config_file_path, + QWidget *parent = nullptr); + ~MainWindow(); + + private slots: + void on_button_start_clicked(); + void on_button_add_custom_mod_clicked(); + void on_button_remove_custom_mod_clicked(); + + void on_checkbox_extra_threads_stateChanged(int arg1); + void on_checkbox_cpu_count_stateChanged(int arg1); + void on_checkbox_parameter_file_stateChanged(int arg1); + void on_checkbox_world_stateChanged(int arg1); + void on_checkbox_custom_parameters_stateChanged(int arg1); + void on_checkbox_server_address_stateChanged(int arg1); + void on_checkbox_server_port_stateChanged(int arg1); + void on_checkbox_server_password_stateChanged(int arg1); + + void check_if_arma_is_running(); + + void on_checkbox_profile_stateChanged(int arg1); + + void on_button_parameter_file_open_clicked(); + + void on_button_mod_preset_open_clicked(); + + void on_button_mod_preset_save_clicked(); + +private: + Ui::MainWindow *ui; + QTimer arma_status_checker; + + std::unique_ptr client; + + std::filesystem::path config_file; + Settings manager; + + void add_item(QTableWidget &table_widget, UiMod const &mod); + void initialize_table_widget(QTableWidget &table_widget, QStringList const &column_names); + + std::vector get_mods(QTableWidget const &table_widget); + UiMod get_mod_from_nth_row(QTableWidget const &table_widget, int row); + void put_mods_from_ui_to_manager_settings(); + + void checkbox_changed(int state); +}; diff --git a/src/arma3-unix-launcher/mainwindow.ui b/src/arma3-unix-launcher/mainwindow.ui new file mode 100644 index 0000000..d449d74 --- /dev/null +++ b/src/arma3-unix-launcher/mainwindow.ui @@ -0,0 +1,604 @@ + + + MainWindow + + + + 0 + 0 + 533 + 645 + + + + Arma 3 Unix Launcher + + + + + + + + + + 0 + 0 + + + + + 500 + 500 + + + + 0 + + + + Mods + + + + + + + + + 1024 + 1024 + + + + QFrame::Box + + + QFrame::Plain + + + <b>Workshop mods</b><br/> +<small>Here you can see mods you are subscribed to</small> + + + Qt::AlignCenter + + + + + + + QAbstractItemView::SelectRows + + + 3 + + + false + + + false + + + + + + + + + + QFrame::Box + + + QFrame::Plain + + + <b>Custom mods</b><br/><small>Here you can add your own mods</small> + + + Qt::AlignCenter + + + + + + + QFrame::StyledPanel + + + QAbstractItemView::SelectRows + + + 3 + + + false + + + false + + + + + + + + + + + + Custom mods: + + + + + + + Add + + + + :/icons/open-iconic/plus.svg:/icons/open-iconic/plus.svg + + + + + + + Remove + + + + :/icons/open-iconic/minus.svg:/icons/open-iconic/minus.svg + + + + + + + + + + + Mod preset: + + + + + + + Open + + + + :/icons/open-iconic/envelope-open.svg:/icons/open-iconic/envelope-open.svg + + + + + + + Save + + + + :/icons/open-iconic/envelope-closed.svg:/icons/open-iconic/envelope-closed.svg + + + + + + + + + + + QFrame::Box + + + 0 + + + Selected 0 mods (0 from workshop, 0 custom) + + + Qt::AlignCenter + + + + + + + + + + + Start + + + + + + + Status: Arma 3 is not running + + + Qt::AlignCenter + + + + + + + + + + + + Parameters + + + + + + QTabWidget::Rounded + + + 0 + + + true + + + + Basic + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Skip intro + + + + + + + Skip logos at startup + + + + + + + Force window mode + + + + + + + + + Profile + + + + + + + false + + + + + + + + + + + + Advanced + + + + + + + + + + Parameter file + + + + + + + false + + + + + + + false + + + Open + + + + + + + + + Check signatures + + + + + + + + + CPU count + + + + + + + false + + + 1 + + + + + + + + + + + Extra threads + + + + + + + + + false + + + File operations + + + + + + + false + + + Texture loading + + + + + + + false + + + Geometry loading + + + + + + + + + + + Enable Hyper-Threading + + + + + + + Enable file patching + + + + + + + No logs + + + + + + + + + World + + + + + + + false + + + + + + + + + No pause + + + + + + + + + Custom parameters + + + + + + + false + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Client + + + + + + + + + + Server address + + + + + + + false + + + + + + + + + + + Server port + + + + + + + false + + + + + + + + + + + Server password + + + + + + + false + + + + + + + + + Host session + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/arma3-unix-launcher/open-iconic b/src/arma3-unix-launcher/open-iconic new file mode 120000 index 0000000..1dee89b --- /dev/null +++ b/src/arma3-unix-launcher/open-iconic @@ -0,0 +1 @@ +../../external/open-iconic \ No newline at end of file diff --git a/src/arma3-unix-launcher/settings.cpp b/src/arma3-unix-launcher/settings.cpp new file mode 100644 index 0000000..26d8368 --- /dev/null +++ b/src/arma3-unix-launcher/settings.cpp @@ -0,0 +1,249 @@ +#include "settings.hpp" + +#include +#include +#include + +#include "std_utils.hpp" + +#include "ui_mainwindow.h" + +namespace +{ + void create_default_config(std::filesystem::path const &config_file) + { + if (std::filesystem::exists(config_file)) + return; + std::filesystem::create_directories(config_file.parent_path()); + StdUtils::CreateFile(config_file); + + nlohmann::json json = nlohmann::json::parse(R"json( + { + "mods": { + "custom": [], + "workshop": [] + }, + "parameters": { + "checkSignatures": false, + "connect": null, + "cpuCount": -1, + "customParameters": null, + "enableHT": false, + "exThreads": -1, + "filePatching": false, + "host": false, + "name": null, + "noLogs": false, + "noPause": false, + "noSplash": false, + "par": null, + "password": null, + "port": null, + "skipIntro": false, + "window": false, + "world": null + } + })json"); + StdUtils::FileWriteAllText(config_file, json.dump(4)); + } +} + +Settings::Settings(std::filesystem::path const config_file_path) : config_file(config_file_path) +{ + create_default_config(config_file); + try + { + settings = nlohmann::json::parse(StdUtils::FileReadAllText(config_file)); + } + catch (std::exception const &ex) + { + fmt::print("Error loading settings from {}:\n{}\n", config_file.string(), ex.what()); + } +} + +std::string Settings::get_launch_parameters() +{ + std::string ret; + for (auto const ¶meter : settings["parameters"].items()) + { + if (parameter.key() == "customParameters") + continue; + + if (parameter.value().type() == nlohmann::json::value_t::boolean && parameter.value()) + ret += fmt::format(" -{}", parameter.key()); + else if (parameter.value().type() == nlohmann::json::value_t::string) + { + std::string value = parameter.value(); + ret += fmt::format(" -{}={}", parameter.key(), value); + } + else if ((parameter.value().type() == nlohmann::json::value_t::number_integer + || parameter.value().type() == nlohmann::json::value_t::number_unsigned) && parameter.value() != -1) + { + int value = parameter.value(); + ret += fmt::format(" -{}={}", parameter.key(), value); + } + } + return ret; +} + +void Settings::save_settings_to_disk() +{ + StdUtils::FileWriteAllText(config_file, settings.dump(4)); +} + +void Settings::load_settings_to_ui(Ui::MainWindow *ui) +{ + // basic tab + read_setting("skipIntro", ui->checkbox_skip_intro); + read_setting("noSplash", ui->checkbox_skip_logos_at_startup); + read_setting("window", ui->checkbox_force_window_mode); + read_setting("name", ui->checkbox_profile, ui->textbox_profile); + + // advanced tab + read_setting("par", ui->checkbox_parameter_file, ui->textbox_parameter_file); + read_setting("checkSignatures", ui->checkbox_check_signatures); + read_setting("enableHT", ui->checkbox_enable_hyper_threading); + read_setting("filePatching", ui->checkbox_enable_file_patching); + read_setting("noLogs", ui->checkbox_no_logs); + read_setting("world", ui->checkbox_world, ui->textbox_world); + read_setting("noPause", ui->checkbox_no_pause); + read_setting("customParameters", ui->checkbox_custom_parameters, ui->textbox_custom_parameters); + + auto ¶meters = settings["parameters"]; + try + { + int cpuCount = parameters.at("cpuCount"); + if (cpuCount != -1) + { + ui->checkbox_cpu_count->setChecked(true); + ui->spinbox_cpu_count->setValue(cpuCount); + } + else + { + ui->checkbox_cpu_count->setChecked(false); + ui->spinbox_cpu_count->setValue(4); + } + } + catch (std::exception const &ex) + { + fmt::print("exception while parsing settings[\"cpuCount\"]:\n{}\n", ex.what()); + } + try + { + int exThreads = parameters.at("exThreads"); + if (exThreads != -1) + { + ui->checkbox_extra_threads->setChecked(true); + ui->checkbox_extra_threads_file_operations->setChecked(exThreads & 1); + ui->checkbox_extra_threads_texture_loading->setChecked(exThreads & 2); + ui->checkbox_extra_threads_geometry_loading->setChecked(exThreads & 4); + } + else + { + + ui->checkbox_extra_threads->setChecked(false); + ui->checkbox_extra_threads_file_operations->setChecked(false); + ui->checkbox_extra_threads_texture_loading->setChecked(false); + ui->checkbox_extra_threads_geometry_loading->setChecked(false); + } + } + catch (std::exception const &ex) + { + fmt::print("exception while parsing settings[\"exThreads\"]:\n{}\n", ex.what()); + } + + // client tab + read_setting("connect", ui->checkbox_server_address, ui->textbox_server_address); + read_setting("port", ui->checkbox_server_port, ui->textbox_server_port); + read_setting("password", ui->checkbox_server_password, ui->textbox_server_password); + read_setting("host", ui->checkbox_host_session); +} + +void Settings::save_settings_from_ui(Ui::MainWindow *ui) +{ + auto ¶meters = settings["parameters"]; + + // basic tab + write_setting("skipIntro", ui->checkbox_skip_intro); + write_setting("noSplash", ui->checkbox_skip_logos_at_startup); + write_setting("window", ui->checkbox_force_window_mode); + write_setting("name", ui->checkbox_profile, ui->textbox_profile); + + // advanced tab + write_setting("par", ui->checkbox_parameter_file, ui->textbox_parameter_file); + write_setting("checkSignatures", ui->checkbox_check_signatures); + write_setting("enableHT", ui->checkbox_enable_hyper_threading); + write_setting("filePatching", ui->checkbox_enable_file_patching); + write_setting("noLogs", ui->checkbox_no_logs); + write_setting("world", ui->checkbox_world, ui->textbox_world); + write_setting("noPause", ui->checkbox_no_pause); + write_setting("customParameters", ui->checkbox_custom_parameters, ui->textbox_custom_parameters); + + parameters["cpuCount"] = -1; + if (ui->checkbox_cpu_count->isChecked()) + parameters["cpuCount"] = ui->spinbox_cpu_count->value(); + + parameters["exThreads"] = -1; + if (ui->checkbox_extra_threads->isChecked()) + { + int value = 0; + value += ui->checkbox_extra_threads_file_operations->isChecked() * 1; + value += ui->checkbox_extra_threads_texture_loading->isChecked() * 2; + value += ui->checkbox_extra_threads_geometry_loading->isChecked() * 4; + parameters["exThreads"] = value; + } + + // client tab + write_setting("connect", ui->checkbox_server_address, ui->textbox_server_address); + write_setting("port", ui->checkbox_server_password, ui->textbox_server_port); + write_setting("password", ui->checkbox_server_password, ui->textbox_server_password); + write_setting("host", ui->checkbox_host_session); +} + +void Settings::read_setting(char const *const setting_name, QCheckBox *checkbox, QLineEdit *textbox) +{ + auto const ¶meters = settings["parameters"]; + try + { + if (textbox != nullptr) + { + if (parameters[setting_name].is_null()) + { + checkbox->setChecked(false); + textbox->setText(""); + + } + else + { + checkbox->setChecked(true); + textbox->setText(QString::fromStdString(parameters[setting_name])); + } + } + else + checkbox->setChecked(parameters.at(setting_name)); + } + catch (std::exception const &ex) + { + fmt::print("exception while parsing settings[\"{}\"]:\n{}\n", setting_name, ex.what()); + } +} + +void Settings::write_setting(char const *const setting_name, QCheckBox *checkbox, QLineEdit *textbox) +{ + try + { + auto ¶meters = settings["parameters"]; + if (textbox != nullptr) + { + parameters[setting_name] = nlohmann::json(); + if (checkbox->isChecked()) + parameters[setting_name] = textbox->text().toStdString(); + } + else + parameters[setting_name] = checkbox->isChecked(); + } + catch (std::exception const &ex) + { + fmt::print("exception while saving settings[\"{}\"]:\n{}\n", setting_name, ex.what()); + } +} diff --git a/src/arma3-unix-launcher/settings.hpp b/src/arma3-unix-launcher/settings.hpp new file mode 100644 index 0000000..da13ef0 --- /dev/null +++ b/src/arma3-unix-launcher/settings.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include + +namespace Ui +{ + class MainWindow; +} + +class QCheckBox; +class QLineEdit; + +class Settings +{ + public: + explicit Settings(std::filesystem::path const config_file_path); + + nlohmann::json settings; + + std::string get_launch_parameters(); + + void load_settings_to_ui(Ui::MainWindow *ui); + void save_settings_from_ui(Ui::MainWindow *ui); + void save_settings_to_disk(); + private: + std::filesystem::path config_file; + + void read_setting(char const *const setting_name, QCheckBox *checkbox, QLineEdit *textbox = nullptr); + void write_setting(char const *const setting_name, QCheckBox *checkbox, QLineEdit *textbox = nullptr); +}; diff --git a/src/arma3-unix-launcher/ui_mod.hpp b/src/arma3-unix-launcher/ui_mod.hpp new file mode 100644 index 0000000..ad59ca4 --- /dev/null +++ b/src/arma3-unix-launcher/ui_mod.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +struct UiMod +{ + bool enabled; + std::string name; + std::string path_or_workshop_id; +}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..8c8d9fa --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,8 @@ +enable_testing() + +option(TRACE_MOCKS "Enable mock tracing (logs every call to mock object to stdout)" OFF) +if (TRACE_MOCKS) + string(APPEND CMAKE_CXX_FLAGS " -include ${CMAKE_CURRENT_SOURCE_DIR}/mock/mock_tracer.hpp") +endif() + +add_subdirectory(arma3-unix-launcher-library) diff --git a/tests/arma3-unix-launcher-library/CMakeLists.txt b/tests/arma3-unix-launcher-library/CMakeLists.txt new file mode 100644 index 0000000..d7f44f3 --- /dev/null +++ b/tests/arma3-unix-launcher-library/CMakeLists.txt @@ -0,0 +1,106 @@ +enable_testing() + +create_test(TEST_GROUP library + TEST_NAME arma3client + SOURCES test_arma3client.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/arma3client.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/arma3client.hpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/string_utils.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/string_utils.hpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/exceptions/file_not_found.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/exceptions/file_not_found.hpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/exceptions/path_no_access.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/exceptions/path_no_access.hpp + + default_test_reporter.hpp + ${CMAKE_SOURCE_DIR}/tests/mock/filesystem_utils.hpp + ${CMAKE_SOURCE_DIR}/tests/mock/std_utils.hpp + ${CMAKE_SOURCE_DIR}/tests/mock/vdf.hpp + INCLUDES ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library + LINK_LIBS fmt::fmt + ) + +create_test(TEST_GROUP library + TEST_NAME cppfilter + SOURCES test_cppfilter.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/cppfilter.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/cppfilter.hpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/exceptions/syntax_error.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/exceptions/syntax_error.hpp + default_test_reporter.hpp + INCLUDES ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library + LINK_LIBS fmt::fmt + ) + +create_test(TEST_GROUP library + TEST_NAME mod + SOURCES test_mod.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/mod.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/mod.hpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/string_utils.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/string_utils.hpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/exceptions/directory_not_found.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/exceptions/directory_not_found.hpp + + default_test_reporter.hpp + ${CMAKE_SOURCE_DIR}/tests/mock/filesystem_utils.hpp + ${CMAKE_SOURCE_DIR}/tests/mock/std_utils.hpp + ${CMAKE_SOURCE_DIR}/tests/mock/vdf.hpp + INCLUDES ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library + LINK_LIBS fmt::fmt + ) + +create_test(TEST_GROUP library + TEST_NAME std_utils + SOURCES test_std_utils.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/std_utils.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/std_utils.hpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/string_utils.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/string_utils.hpp + default_test_reporter.hpp + INCLUDES ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library + LINK_LIBS fmt::fmt + ) + +create_test(TEST_GROUP library + TEST_NAME string_utils + SOURCES test_string_utils.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/string_utils.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/string_utils.hpp + default_test_reporter.hpp + INCLUDES ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library + LINK_LIBS fmt::fmt + ) + +create_test(TEST_GROUP library + TEST_NAME steam + SOURCES test_steam.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/steam.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/steam.hpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/string_utils.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/string_utils.hpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/exceptions/steam_install_not_found.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/exceptions/steam_install_not_found.hpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/exceptions/steam_workshop_directory_not_found.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/exceptions/steam_workshop_directory_not_found.hpp + + default_test_reporter.hpp + ${CMAKE_SOURCE_DIR}/tests/mock/filesystem_utils.hpp + ${CMAKE_SOURCE_DIR}/tests/mock/std_utils.hpp + ${CMAKE_SOURCE_DIR}/tests/mock/vdf.hpp + INCLUDES ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library + LINK_LIBS fmt::fmt + ) + +create_test(TEST_GROUP library + TEST_NAME vdf + SOURCES test_vdf.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/vdf.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/vdf.hpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/exceptions/syntax_error.cpp + ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library/exceptions/syntax_error.hpp + + default_test_reporter.hpp + INCLUDES ${CMAKE_SOURCE_DIR}/src/arma3-unix-launcher-library + LINK_LIBS fmt::fmt + ) diff --git a/tests/arma3-unix-launcher-library/default_test_reporter.hpp b/tests/arma3-unix-launcher-library/default_test_reporter.hpp new file mode 100644 index 0000000..af8f66e --- /dev/null +++ b/tests/arma3-unix-launcher-library/default_test_reporter.hpp @@ -0,0 +1,38 @@ +#pragma once +#include + +struct DefaultTestReporter : public doctest::IReporter +{ + std::ostream& stdout_stream; + doctest::ContextOptions const& opt; + + DefaultTestReporter(doctest::ContextOptions const& in) + : stdout_stream(*in.cout) + , opt(in) {} + + void test_case_start(doctest::TestCaseData const& in) override + { + stdout_stream << "[TEST STARTED] " << in.m_name << '\n'; + } + + void test_case_end(doctest::CurrentTestCaseStats const& in) override + { + if (in.failure_flags == 0) + stdout_stream << "[TEST SUCCESS]\n"; + else + stdout_stream << "[TEST FAILURE]\n"; + } + + /* useless functions because virtual interface is purely virtual :D */ + void report_query(doctest::QueryData const&) override {} + void test_run_start() override {} + void test_run_end(doctest::TestRunStats const&) override {} + void test_case_reenter(doctest::TestCaseData const&) override {} + void test_case_exception(doctest::TestCaseException const&) override {} + void subcase_start(doctest::SubcaseSignature const&) override {} + void subcase_end() override {} + void log_assert(doctest::AssertData const&) override {} + void log_message(doctest::MessageData const&) override {} + void test_case_skipped(doctest::TestCaseData const&) override {} +}; +REGISTER_LISTENER("gtest_alike_printer", 1, DefaultTestReporter); diff --git a/tests/arma3-unix-launcher-library/test_arma3client.cpp b/tests/arma3-unix-launcher-library/test_arma3client.cpp new file mode 100644 index 0000000..cd9eb81 --- /dev/null +++ b/tests/arma3-unix-launcher-library/test_arma3client.cpp @@ -0,0 +1,388 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include +#include +#include "default_test_reporter.hpp" + +#include "arma3client.hpp" +#include "exceptions/file_not_found.hpp" + +#include "cppfilter.hpp" +#include "filesystem_utils.hpp" +#include "mod.hpp" +#include "std_utils.hpp" +#include "vdf.hpp" +#include "mock/cppfilter.hpp" +#include "mock/filesystem_utils.hpp" +#include "mock/mod.hpp" +#include "mock/std_utils.hpp" +#include "mock/vdf.hpp" + +using trompeloeil::_; + +class ARMA3ClientTests +{ + public: + CppFilterMock cppFilterMock; + FilesystemUtilsMock filesystemUtilsMock; + ModMock modMock; + StdUtilsMock stdUtilsMock; + VdfMock vdfMock; + + std::filesystem::path const arma_path = "/arma_path"; + std::filesystem::path const workshop_path = "/workshop_path"; + std::string const mac_executable_name = "ArmA3.app"; + std::string const linux_executable_name = "arma3.x86_64"; + std::string const proton_executable_name = "arma3_x64.exe"; + + std::filesystem::path get_executable_path() + { +#ifdef __linux__ + return arma_path / linux_executable_name; +#else + return arma_path / mac_executable_name; +#endif + } +}; + +#ifdef __linux__ +TEST_CASE_FIXTURE(ARMA3ClientTests, "Constructor_Linux") +{ + GIVEN("Arma and workshop path") + { + WHEN("Arma path contains ARMA 3 Linux executable") + { + REQUIRE_CALL(filesystemUtilsMock, Exists(arma_path / linux_executable_name)).RETURN(true); + THEN("ARMA3::Client is created") + { + ARMA3::Client a3c(arma_path, workshop_path); + } + } + WHEN("Arma path contains ARMA 3 Proton executable") + { + REQUIRE_CALL(filesystemUtilsMock, Exists(arma_path / linux_executable_name)).RETURN(false); + REQUIRE_CALL(filesystemUtilsMock, Exists(arma_path / proton_executable_name)).RETURN(true); + THEN("ARMA3::Client is created") + { + ARMA3::Client a3c(arma_path, workshop_path); + } + } + WHEN("Arma path does not contain ARMA 3 executables") + { + REQUIRE_CALL(filesystemUtilsMock, Exists(arma_path / linux_executable_name)).RETURN(false); + REQUIRE_CALL(filesystemUtilsMock, Exists(arma_path / proton_executable_name)).RETURN(false); + THEN("Exception is thrown") + { + CHECK_THROWS_AS(ARMA3::Client(arma_path, workshop_path), FileNotFoundException); + } + } + } +} +#endif + +#ifdef __APPLE__ +TEST_CASE_FIXTURE(ARMA3ClientTests, "Constructor_Mac_OS_X") +{ + GIVEN("Arma and workshop path") + { + WHEN("Arma path contains ARMA 3 Mac OS X executable") + { + REQUIRE_CALL(filesystemUtilsMock, Exists(arma_path / mac_executable_name)).RETURN(true); + THEN("ARMA3::Client is created") + { + ARMA3::Client a3c(arma_path, workshop_path); + } + } + WHEN("Arma path does not contain ARMA 3 executable") + { + REQUIRE_CALL(filesystemUtilsMock, Exists(arma_path / mac_executable_name)).RETURN(false); + THEN("Exception is thrown") + { + CHECK_THROWS_AS(ARMA3::Client(arma_path, workshop_path), FileNotFoundException); + } + } + } +} +#endif + +#ifdef __linux__ +TEST_CASE_FIXTURE(ARMA3ClientTests, "IsProton_Linux") +{ + GIVEN("Arma 3 Client") + { + WHEN("Arma path contains ARMA 3 Linux executable") + { + REQUIRE_CALL(filesystemUtilsMock, Exists(arma_path / linux_executable_name)).RETURN(true); + ARMA3::Client a3c(arma_path, workshop_path); + THEN("isProton() is false") + { + CHECK_FALSE(a3c.IsProton()); + } + } + WHEN("Arma path contains ARMA 3 Proton executable") + { + REQUIRE_CALL(filesystemUtilsMock, Exists(arma_path / linux_executable_name)).RETURN(false); + REQUIRE_CALL(filesystemUtilsMock, Exists(arma_path / proton_executable_name)).RETURN(true); + ARMA3::Client a3c(arma_path, workshop_path); + THEN("isProton() is true") + { + CHECK(a3c.IsProton()); + } + } + } +} +#endif + +#ifdef __APPLE__ +TEST_CASE_FIXTURE(ARMA3ClientTests, "IsProton_Mac_OS_X") +{ + GIVEN("Arma 3 Client") + { + REQUIRE_CALL(filesystemUtilsMock, Exists(arma_path / mac_executable_name)).RETURN(true); + ARMA3::Client a3c(arma_path, workshop_path); + WHEN("Arma path contains ARMA 3 Mac OS X executable") + { + THEN("isProton() is false") + { + CHECK_FALSE(a3c.IsProton()); + } + } + } +} +#endif + +TEST_CASE_FIXTURE(ARMA3ClientTests, "GetHomeMods") +{ + GIVEN("Arma's home directory path and ARMA3::Client") + { + REQUIRE_CALL(filesystemUtilsMock, Exists(get_executable_path())).RETURN(true); + ARMA3::Client a3c(arma_path, workshop_path); + WHEN("Arma's home directory does not contain mods") + { + REQUIRE_CALL(filesystemUtilsMock, Ls(arma_path, _)).RETURN(std::vector{}); + THEN("Returned vector is empty") + { + CHECK(a3c.GetHomeMods().empty()); + } + } + WHEN("Arma's home directory contains only excluded directories") + { + REQUIRE_CALL(filesystemUtilsMock, Ls(arma_path, _)).RETURN(std::vector{"Addons", "Expansion"}); + THEN("Returned vector is empty") + { + CHECK(a3c.GetHomeMods().empty()); + } + } + WHEN("Arma's home directory contains only some excluded directories and two valid mods") + { + std::array const mod_names{"@othermod", "@Remove_stamina"}; + std::array const mod_paths{arma_path / mod_names[0], arma_path / mod_names[1]}; + + REQUIRE_CALL(filesystemUtilsMock, Ls(arma_path, _)).RETURN(std::vector{"Addons", "Expansion", mod_names[0], mod_names[1]}); + REQUIRE_CALL(filesystemUtilsMock, IsDirectory(mod_paths[0])).RETURN(true); + REQUIRE_CALL(filesystemUtilsMock, Ls(mod_paths[0], _)).RETURN(std::vector{"addons"}); + REQUIRE_CALL(modMock, Constructor(mod_paths[0], _)); + REQUIRE_CALL(filesystemUtilsMock, IsDirectory(mod_paths[1])).RETURN(true); + REQUIRE_CALL(filesystemUtilsMock, Ls(mod_paths[1], _)).RETURN(std::vector{"addons"}); + REQUIRE_CALL(modMock, Constructor(mod_paths[1], _)); + THEN("Returned vector contains two valid mods") + { + auto mods = a3c.GetHomeMods(); + CHECK_EQ(mod_paths[0], mods[0].path_); + CHECK_EQ(mod_paths[1], mods[1].path_); + } + } + } +} + +TEST_CASE_FIXTURE(ARMA3ClientTests, "GetWorkshopMods") +{ + GIVEN("Arma's workshop directory path and ARMA3::Client") + { + REQUIRE_CALL(filesystemUtilsMock, Exists(get_executable_path())).RETURN(true); + ARMA3::Client a3c(arma_path, workshop_path); + WHEN("Workshop directory does not contain mods") + { + REQUIRE_CALL(filesystemUtilsMock, Ls(workshop_path, _)).RETURN(std::vector{}); + THEN("Returned vector is empty") + { + CHECK(a3c.GetWorkshopMods().empty()); + } + } + WHEN("Workshop directory contains only two directories with useless files inside") + { + std::array const mod_names{"123", "456"}; + std::array const mod_paths{workshop_path / mod_names[0], workshop_path / mod_names[1]}; + REQUIRE_CALL(filesystemUtilsMock, Ls(workshop_path, _)).RETURN(std::vector{mod_paths[0], mod_paths[1]}); + REQUIRE_CALL(filesystemUtilsMock, IsDirectory(mod_paths[0])).RETURN(true); + REQUIRE_CALL(filesystemUtilsMock, Ls(mod_paths[0], _)).RETURN(std::vector{"useless.bin"}); + REQUIRE_CALL(filesystemUtilsMock, IsDirectory(mod_paths[1])).RETURN(true); + REQUIRE_CALL(filesystemUtilsMock, Ls(mod_paths[1], _)).RETURN(std::vector{"useless.bin"}); + THEN("Returned vector is empty") + { + CHECK(a3c.GetWorkshopMods().empty()); + } + } + WHEN("Workshop directory contains two directories with workshop mods inside") + { + std::array const mod_names{"123", "456"}; + std::array const mod_paths{workshop_path / mod_names[0], workshop_path / mod_names[1]}; + REQUIRE_CALL(filesystemUtilsMock, Ls(workshop_path, _)).RETURN(std::vector{mod_paths[0], mod_paths[1]}); + REQUIRE_CALL(filesystemUtilsMock, IsDirectory(mod_paths[0])).RETURN(true); + REQUIRE_CALL(filesystemUtilsMock, Ls(mod_paths[0], _)).RETURN(std::vector{"addons"}); + REQUIRE_CALL(modMock, Constructor(mod_paths[0], _)); + REQUIRE_CALL(filesystemUtilsMock, IsDirectory(mod_paths[1])).RETURN(true); + REQUIRE_CALL(filesystemUtilsMock, Ls(mod_paths[1], _)).RETURN(std::vector{"addons"}); + REQUIRE_CALL(modMock, Constructor(mod_paths[1], _)); + THEN("Returned vector is empty") + { + auto mods = a3c.GetWorkshopMods(); + CHECK_EQ(mod_paths[0], mods[0].path_); + CHECK_EQ(mod_paths[1], mods[1].path_); + } + } + } +} + +TEST_CASE_FIXTURE(ARMA3ClientTests, "CreateArmaCfg") +{ + GIVEN("ARMA3::Client with valid home directory structure and valid mod list") + { + std::string const config_file_mod_part = R"cpp(class ModLauncherList +{ + class Mod1 + { + dir="123"; + name="Remove Stamina"; + origin="GAME DIR"; + fullPath="C:\workshop_path\123"; + }; + class Mod2 + { + dir="456"; + name="Big Mod"; + origin="GAME DIR"; + fullPath="C:\workshop_path\456"; + }; +}; +)cpp"; + std::filesystem::path const random_directory = "/random"; + std::filesystem::path const random_config_path = "/random/config.cfg"; + + REQUIRE_CALL(filesystemUtilsMock, Exists(get_executable_path())).RETURN(true); + ARMA3::Client a3c(arma_path, workshop_path); + + std::filesystem::path remove_stamina_path = workshop_path / "123"; + REQUIRE_CALL(modMock, Constructor(remove_stamina_path, _)).SIDE_EFFECT(_2.KeyValue["name"] = "Remove Stamina"); + + std::filesystem::path big_mod_path = workshop_path / "456"; + REQUIRE_CALL(modMock, Constructor(big_mod_path, _)).SIDE_EFFECT(_2.KeyValue["name"] = "Big Mod"); + + WHEN("Arma config file does not exist but config file's parent directory exists") + { + REQUIRE_CALL(filesystemUtilsMock, Exists(random_config_path)).RETURN(false); + REQUIRE_CALL(filesystemUtilsMock, Exists(random_directory)).RETURN(true); + REQUIRE_CALL(stdUtilsMock, CreateFile(random_config_path)).RETURN(true); + REQUIRE_CALL(stdUtilsMock, FileReadAllText(random_config_path)).RETURN(""); + + THEN("Config file is created, containing only mods") + { + REQUIRE_CALL(cppFilterMock, RemoveClass("class ModLauncherList", _)).RETURN(""); + REQUIRE_CALL(stdUtilsMock, FileWriteAllText(random_config_path, config_file_mod_part)); + a3c.CreateArmaCfg({"123", "456"}, {}, random_config_path); + } + } + + WHEN("Arma config file does not exist and config file's parent directory does not exist") + { + REQUIRE_CALL(filesystemUtilsMock, Exists(random_config_path)).RETURN(false); + REQUIRE_CALL(filesystemUtilsMock, Exists(random_directory)).RETURN(false); + REQUIRE_CALL(filesystemUtilsMock, CreateDirectories(random_directory)).RETURN(true); + REQUIRE_CALL(stdUtilsMock, CreateFile(random_config_path)).RETURN(true); + REQUIRE_CALL(stdUtilsMock, FileReadAllText(random_config_path)).RETURN(""); + + THEN("Config directory is created and config file is created, containing only mods") + { + REQUIRE_CALL(cppFilterMock, RemoveClass("class ModLauncherList", _)).RETURN(""); + REQUIRE_CALL(stdUtilsMock, FileWriteAllText(random_config_path, config_file_mod_part)); + a3c.CreateArmaCfg({"123", "456"}, {}, random_config_path); + } + } + + WHEN("Arma config file exists and contains some entries") + { + std::string const entries = R"cpp(setting="one"; +someInt=5; +)cpp"; + std::string const expected_config_file = entries + config_file_mod_part; + + REQUIRE_CALL(filesystemUtilsMock, Exists(random_config_path)).RETURN(true); + REQUIRE_CALL(stdUtilsMock, FileReadAllText(random_config_path)).RETURN(entries); + + THEN("Config directory is created and config file is created, containing mods and previous entries") + { + REQUIRE_CALL(cppFilterMock, RemoveClass("class ModLauncherList", _)).RETURN(entries); + REQUIRE_CALL(stdUtilsMock, FileWriteAllText(random_config_path, expected_config_file)); + a3c.CreateArmaCfg({"123", "456"}, {}, random_config_path); + } + } + } +} + +#ifdef __linux__ +TEST_CASE_FIXTURE(ARMA3ClientTests, "Start_Linux") +{ + GIVEN("Arma 3 Client") + { + std::string const arguments = "some random arguments"; + std::string const steam_command = "steam -applaunch 107410"; + + WHEN("Arma path contains ARMA 3 Linux executable") + { + REQUIRE_CALL(filesystemUtilsMock, Exists(arma_path / linux_executable_name)).RETURN(true); + ARMA3::Client a3c(arma_path, workshop_path); + THEN("Arma is started with passed arguments preceded by -nolauncher option") + { + std::string const launch_command = fmt::format("{} {}", steam_command, arguments); + + REQUIRE_CALL(stdUtilsMock, StartBackgroundProcess(launch_command)); + a3c.Start(arguments); + } + } + WHEN("Arma path contains ARMA 3 Proton executable") + { + REQUIRE_CALL(filesystemUtilsMock, Exists(arma_path / linux_executable_name)).RETURN(false); + REQUIRE_CALL(filesystemUtilsMock, Exists(arma_path / proton_executable_name)).RETURN(true); + ARMA3::Client a3c(arma_path, workshop_path); + THEN("Arma is started with passed arguments") + { + std::string const launch_command = fmt::format("{} -nolauncher {}", steam_command, arguments); + + REQUIRE_CALL(stdUtilsMock, StartBackgroundProcess(launch_command)); + a3c.Start(arguments); + } + } + } +} +#endif + +#ifdef __APPLE__ +TEST_CASE_FIXTURE(ARMA3ClientTests, "Start_Mac_OS_X") +{ + GIVEN("Arma 3 Client") + { + std::string const arguments = "some random arguments"; + std::string const arguments_whitespaces_replaced = "some%20random%20arguments"; + std::string const steam_command = "open steam://run/107410//"; + + REQUIRE_CALL(filesystemUtilsMock, Exists(get_executable_path())).RETURN(true); + ARMA3::Client a3c(arma_path, workshop_path); + THEN("Arma is started with passed arguments") + { + std::string const launch_command = fmt::format("{}{}", steam_command, arguments_whitespaces_replaced); + REQUIRE_CALL(stdUtilsMock, StartBackgroundProcess(launch_command)); + a3c.Start(arguments); + } + } +} +#endif diff --git a/tests/arma3-unix-launcher-library/test_cppfilter.cpp b/tests/arma3-unix-launcher-library/test_cppfilter.cpp new file mode 100644 index 0000000..6b9edc0 --- /dev/null +++ b/tests/arma3-unix-launcher-library/test_cppfilter.cpp @@ -0,0 +1,210 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include + +#include "default_test_reporter.hpp" + +#include "cppfilter.hpp" + +std::string const valid_config_file = R"cpp(steamLanguage=""; +language="English"; +forcedAdapterId=-1; +detectedAdapterId=2; +detectedAdapterVendorId=1; +detectedAdapterDeviceId=1; +detectedAdapterSubSysId=1; +detectedAdapterRevision=1; +detectedAdapterBenchmark=32; +detectedCPUBenchmark=81; +detectedCPUFrequency=4200; +detectedCPUCores=8; +detectedCPUDescription="Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz"; +winX=16; +winY=32; +winWidth=1280; +winHeight=960; +winDefWidth=1024; +winDefHeight=768; +fullScreenWidth=1680; +fullScreenHeight=1050; +enderWidth=1280; +renderHeight=960; +multiSampleCount=8; +multiSampleQuality=0; +particlesQuality=2; +GPU_MaxFramesAhead=1000; +GPU_DetectedFramesAhead=1; +HDRPrecision=16; +vsync=1; +AToC=15; +cloudsQuality=4; +waterSSReflectionsQuality=0; +pipQuality=3; +dynamicLightsQuality=4; +PPAA=7; +ppSSAO=6; +ppCaustics=1; +tripleBuffering=0; +ppBloom=1; +ppRotBlur=1; +ppRadialBlur=1; +ppDOF=1; +ppSharpen=1; +)cpp"; + +std::string const mod_classes = R"cpp(class ModLauncherList +{ + class Mod1 + { + dir="@ALiVE"; + name="Arma 3: ALiVE"; + origin="GAME DIR"; + fullPath="C:\home\muttley\.steam\steam\steamapps\common\Arma 3\!workshop\@ALiVE"; + }; + class Mod2 + { + dir="@CUP Terrains - CWA"; + name="CUP Terrains - CWA 1.4.2"; + origin="GAME DIR"; + fullPath="C:\home\muttley\.steam\steam\steamapps\common\Arma 3\!workshop\@CUP Terrains - CWA"; + }; + class Mod3 + { + dir="@CUP Terrains - Core"; + name="CUP Terrains - Core 1.4.2"; + origin="GAME DIR"; + fullPath="C:\home\muttley\.steam\steam\steamapps\common\Arma 3\!workshop\@CUP Terrains - Core"; + }; + class Mod4 + { + dir="@CUP Terrains - Maps"; + name="CUP Terrains - Maps 1.4.2"; + origin="GAME DIR"; + fullPath="C:\home\muttley\.steam\steam\steamapps\common\Arma 3\!workshop\@CUP Terrains - Maps"; + }; + class Mod5 + { + dir="@CUP Units"; + name="CUP Units 1.10.1"; + origin="GAME DIR"; + fullPath="C:\home\muttley\.steam\steam\steamapps\common\Arma 3\!workshop\@CUP Units"; + }; + class Mod6 + { + dir="@CUP Vehicles"; + name="CUP Vehicles 1.10.1"; + origin="GAME DIR"; + fullPath="C:\home\muttley\.steam\steam\steamapps\common\Arma 3\!workshop\@CUP Vehicles"; + }; + class Mod7 + { + dir="@CUP Weapons"; + name="CUP Weapons 1.10.1"; + origin="GAME DIR"; + fullPath="C:\home\muttley\.steam\steam\steamapps\common\Arma 3\!workshop\@CUP Weapons"; + }; + class Mod8 + { + dir="@CBA_A3"; + name="Community Base Addons v3.9.0"; + origin="GAME DIR"; + fullPath="C:\home\muttley\.steam\steam\steamapps\common\Arma 3\!workshop\@CBA_A3"; + }; + class Mod9 + { + dir="@Enhanced Movement"; + name="Enhanced Movement"; + origin="GAME DIR"; + fullPath="C:\home\muttley\.steam\steam\steamapps\common\Arma 3\!workshop\@Enhanced Movement"; + }; + class Mod10 + { + dir="@RHSAFRF"; + name="RHS: Armed Forces of the Russian Federation"; + origin="GAME DIR"; + fullPath="C:\home\muttley\.steam\steam\steamapps\common\Arma 3\!workshop\@RHSAFRF"; + }; + class Mod11 + { + dir="@RHSGREF"; + name="RHS: GREF"; + origin="GAME DIR"; + fullPath="C:\home\muttley\.steam\steam\steamapps\common\Arma 3\!workshop\@RHSGREF"; + }; + class Mod12 + { + dir="@RHSSAF"; + name="RHS: Serbian Armed Forces"; + origin="GAME DIR"; + fullPath="C:\home\muttley\.steam\steam\steamapps\common\Arma 3\!workshop\@RHSSAF"; + }; + class Mod13 + { + dir="@RHSUSAF"; + name="RHS: United States Forces"; + origin="GAME DIR"; + fullPath="C:\home\muttley\.steam\steam\steamapps\common\Arma 3\!workshop\@RHSUSAF"; + }; +}; +)cpp"; + +std::string const unrelated_classes = R"cpp( +class UnrelatedClass +{ + class SubClass + { + class SubSubClass + { + text="YAY!"; + }; + class SubSubClass2 + { + RandomField="OK"; + }; + }; +}; +)cpp"; + +std::string const error_prone_classes = R"cpp(teststring="\"\nsometext"; +class BadClass +{ + class InheritingClass + { + text="\n\"\"Some text2\""; + text2="SomeRa ndomText"; + }; +}; +)cpp"; + +TEST_CASE("RemoveModLauncherList") +{ + std::array config_files = + { + "", + "some trash", + valid_config_file, + valid_config_file + mod_classes, + valid_config_file + unrelated_classes, + valid_config_file + mod_classes + unrelated_classes, + valid_config_file + error_prone_classes, + valid_config_file + error_prone_classes + mod_classes + }; + + std::array out_files = + { + "", + "some trash", + valid_config_file, + valid_config_file, + valid_config_file + unrelated_classes, + valid_config_file + unrelated_classes, + valid_config_file + error_prone_classes, + valid_config_file + error_prone_classes + }; + + for (size_t i = 0; i < config_files.size(); ++i) + { + CppFilter cpp_filter{config_files[i]}; + CHECK_EQ(out_files[i], cpp_filter.RemoveClass("class ModLauncherList")); + } +} diff --git a/tests/arma3-unix-launcher-library/test_mod.cpp b/tests/arma3-unix-launcher-library/test_mod.cpp new file mode 100644 index 0000000..3f07581 --- /dev/null +++ b/tests/arma3-unix-launcher-library/test_mod.cpp @@ -0,0 +1,157 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include +#include +#include "default_test_reporter.hpp" + +#include "mod.hpp" +#include "exceptions/directory_not_found.hpp" + +#include "filesystem_utils.hpp" +#include "std_utils.hpp" +#include "vdf.hpp" +#include "mock/filesystem_utils.hpp" +#include "mock/std_utils.hpp" +#include "mock/vdf.hpp" + +std::ostream &operator<< (std::ostream &os, std::map const &key_value) +{ + os << "\nMap begin:\n"; + for (auto const& [key, value] : key_value) + os << key << ':' << value << '\n'; + os << "Map end:\n\n"; + return os; +} + +namespace doctest { + template<> struct StringMaker> { + static String convert(std::map const& map) { + std::string output = "map: {\n"; + for (auto const& [key, value]: map) + output += fmt::format("{}: {}\n", key, value); + output += "}\n"; + return output.c_str(); + } + }; +} + +using trompeloeil::_; + +class ModTests +{ +public: + FilesystemUtilsMock filesystemUtilsMock; + StdUtilsMock stdUtilsMock; + VdfMock vdfMock; + + std::filesystem::path const remove_stamina_path = "/remove_stamina"; + std::filesystem::path const remove_stamina_mod_cpp_path = remove_stamina_path / "mod.cpp"; + std::string const remove_stamina_mod_cpp = R"cpp(name = "Remove Stamina"; +dir = "@Remove stamina"; +picture = "logo.paa"; +hidePicture = "false"; +hideName = "false"; +logo = "logo.paa"; +description = "Simple mod which removes stamina from ArmA 3"; +author = "Muttley";)cpp"; + std::string const remove_stamina_mod_cpp_missing_quotes = R"cpp(name = "Remove Stamina"; +dir = "@Remove stamina"; +picture = "logo.paa"; +hidePicture = false; +hideName = false; +logo = "logo.paa"; +description = "Simple mod which removes stamina from ArmA 3"; +author = Muttley; +)cpp"; + std::string const remove_stamina_mod_cpp_no_whitespaces= R"cpp(name="Remove Stamina";dir="@Remove stamina";picture="logo.paa";hidePicture="false";hideName="false";logo="logo.paa";description="Simple mod which removes stamina from ArmA 3";author="Muttley";)cpp"; + std::map const remove_stamina_map + { + {"name", "Remove Stamina"}, + {"dir", "@Remove stamina"}, + {"picture", "logo.paa"}, + {"hidePicture", "false"}, + {"hideName", "false"}, + {"logo", "logo.paa"}, + {"description", "Simple mod which removes stamina from ArmA 3"}, + {"author", "Muttley"}, + {"publishedid", "remove_stamina"} + }; +}; + +TEST_CASE_FIXTURE(ModTests, "Constructor_Success") +{ + GIVEN("Mod directory containing 'addons' subdirectory") + { + REQUIRE_CALL(filesystemUtilsMock, Ls(remove_stamina_path, _)).TIMES(2).RETURN(std::vector{"addons"}); + + WHEN("Subaddons directory exists") + THEN("Mod is constructed, no exception thrown") + Mod mod(remove_stamina_path); + } +} + +TEST_CASE_FIXTURE(ModTests, "Constructor_Failed_MissingAddonsDirectory") +{ + GIVEN("Mod path not containing 'addons' directory") + { + REQUIRE_CALL(filesystemUtilsMock, Ls(remove_stamina_path, true)).RETURN(std::vector{}); + + WHEN("./addons directory does not exist") + THEN("Exception is thrown") + CHECK_THROWS_AS(Mod(std::ref(remove_stamina_path)), DirectoryNotFoundException); //doctest weirdness + } +} + +TEST_CASE_FIXTURE(ModTests, "BasicParsing_Success") +{ + GIVEN("Mod path containing valid 'addons' and 'mod.cpp'") + { + REQUIRE_CALL(filesystemUtilsMock, Ls(remove_stamina_path, _)).TIMES(2).RETURN(std::vector{"addons", "mod.cpp"}); + REQUIRE_CALL(stdUtilsMock, FileReadAllText(remove_stamina_mod_cpp_path)).LR_RETURN(remove_stamina_mod_cpp); + + WHEN("Mod is created") + { + Mod remove_stamina(remove_stamina_path); + THEN("Appropriate keys and values are set") + { + CHECK_EQ(remove_stamina_map, remove_stamina.KeyValue); + } + } + } +} + +TEST_CASE_FIXTURE(ModTests, "MissingQuotes") +{ + GIVEN("Mod path containing valid 'addons' and 'mod.cpp' with missing quotes in one KeyValue pair") + { + REQUIRE_CALL(filesystemUtilsMock, Ls(remove_stamina_path, _)).TIMES(2).RETURN(std::vector{"addons", "mod.cpp"}); + REQUIRE_CALL(stdUtilsMock, FileReadAllText(remove_stamina_mod_cpp_path)).LR_RETURN(remove_stamina_mod_cpp_missing_quotes); + + WHEN("Mod is created") + { + Mod remove_stamina(remove_stamina_path); + THEN("Appropriate keys and values are set, despite missing quotes") + { + CHECK_EQ(remove_stamina_map, remove_stamina.KeyValue); + } + } + } +} + +TEST_CASE_FIXTURE(ModTests, "NoWhitespaces") +{ + GIVEN("Mod path containing valid 'addons' and 'mod.cpp' without whitespaces") + { + REQUIRE_CALL(filesystemUtilsMock, Ls(remove_stamina_path, _)).TIMES(2).RETURN(std::vector{"addons", "mod.cpp"}); + REQUIRE_CALL(stdUtilsMock, FileReadAllText(remove_stamina_mod_cpp_path)).LR_RETURN(remove_stamina_mod_cpp_no_whitespaces); + + WHEN("Mod is created") + { + Mod remove_stamina(remove_stamina_path); + THEN("Appropriate keys and values are set, despite no whitespaces") + { + CHECK_EQ(remove_stamina_map, remove_stamina.KeyValue); + } + } + } +} diff --git a/tests/arma3-unix-launcher-library/test_std_utils.cpp b/tests/arma3-unix-launcher-library/test_std_utils.cpp new file mode 100644 index 0000000..f6cb272 --- /dev/null +++ b/tests/arma3-unix-launcher-library/test_std_utils.cpp @@ -0,0 +1,80 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include + +#include "default_test_reporter.hpp" + +#include "std_utils.hpp" + +TEST_CASE("Contains") +{ + using namespace StdUtils; + + SUBCASE("integers") + { + std::vector numbers{1, 2, 4, 5, 6}; + CHECK(Contains(numbers, 1)); + CHECK(Contains(numbers, 2)); + CHECK_FALSE(Contains(numbers, 3)); + } + + SUBCASE("strings") + { + std::vector strings{"test", "123", "addons"}; + CHECK(Contains(strings, "test")); + CHECK(Contains(strings, "addons")); + CHECK_FALSE(Contains(strings, "ADDONS")); + } +} + +TEST_CASE("ContainsKey") +{ + using namespace StdUtils; + + SUBCASE("int, int") + { + std::map numbers{{0, 1}, {1, 1}, {3, 3}}; + CHECK(ContainsKey(numbers, 0)); + CHECK(ContainsKey(numbers, 1)); + CHECK_FALSE(ContainsKey(numbers, 2)); + } + + SUBCASE("string, string") + { + std::map strings{{"test", "testValue"}, {"testKey", "testValue2"}}; + CHECK(ContainsKey(strings, "test")); + CHECK(ContainsKey(strings, "testKey")); + CHECK_FALSE(ContainsKey(strings, "nothing here")); + } +} + +TEST_CASE("GetConfigFilePath") +{ + using namespace StdUtils; + + GIVEN("Filename to get config path for") + { + std::filesystem::path config_file = "a3unixlauncher.cfg"; + + WHEN("XDG_CONFIG_HOME is not set") + { + REQUIRE_EQ(0, unsetenv("XDG_CONFIG_HOME")); + + auto path = GetConfigFilePath(config_file); + + THEN("$HOME/.config is used") + CHECK_EQ(fmt::format("{}/.config/a3unixlauncher/{}", getenv("HOME"), config_file.string()), path); + } + + WHEN("XDG_CONFIG_HOME is set") + { + std::filesystem::path xdg_config_home = "/configs"; + REQUIRE_EQ(0, setenv("XDG_CONFIG_HOME", xdg_config_home.string().c_str(), 1)); + + auto path = GetConfigFilePath(config_file); + + THEN("XDG_CONFIG_HOME is used") + CHECK_EQ(fmt::format("{}/a3unixlauncher/{}", xdg_config_home.string(), config_file.string()), path); + } + } +} diff --git a/tests/arma3-unix-launcher-library/test_steam.cpp b/tests/arma3-unix-launcher-library/test_steam.cpp new file mode 100644 index 0000000..ea5f950 --- /dev/null +++ b/tests/arma3-unix-launcher-library/test_steam.cpp @@ -0,0 +1,196 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include +#include +#include "default_test_reporter.hpp" + +#include + +#include + +#include "steam.hpp" + +#include "filesystem_utils.hpp" +#include "std_utils.hpp" +#include "vdf.hpp" +#include "mock/filesystem_utils.hpp" +#include "mock/std_utils.hpp" +#include "mock/vdf.hpp" + +#include "exceptions/steam_install_not_found.hpp" +#include "exceptions/steam_workshop_directory_not_found.hpp" + +namespace doctest { + template<> struct StringMaker> { + static String convert(std::vector const& vec) { + std::string output = "vector: {"; + for (auto const& str: vec) + output += fmt::format("\"{}\", ", str); + output += "}\n"; + return output.c_str(); + } + }; + template<> struct StringMaker> { + static String convert(std::vector const& vec) { + std::string output = "vector: {"; + for (auto const& str: vec) + output += fmt::format("\"{}\", ", str.string()); + output += '\n'; + return output.c_str(); + } + }; +} + +using trompeloeil::_; + +class SteamTests +{ +public: + FilesystemUtilsMock filesystemUtilsMock; + StdUtilsMock stdUtilsMock; + VdfMock vdfMock; + + std::filesystem::path const default_steam_path = "/somewhere/steam"; + std::filesystem::path const default_config_path = default_steam_path / "config/config.vdf"; + + std::string const empty_file_content = ""; + std::string const arma3_workshop_id = "107410"; + + std::string_view const vdf_config = R"vdf( +"InstallConfigStore" +{ + "Software" + { + "Valve" + { + "Steam" + { + "BaseInstallFolder_1" "/mnt/games/SteamLibrary" + "BaseInstallFolder_2" "/mnt/disk2/steamgames" + } + } + } +} +)vdf"; + std::vector const steam_library_dirs{"/mnt/games/SteamLibrary", "/mnt/disk2/steamgames"}; +}; + +TEST_CASE_FIXTURE(SteamTests, "Constructor_Success") +{ + GIVEN("Steam config file that exists in correct path") + { + REQUIRE_CALL(filesystemUtilsMock, Exists(default_config_path)).RETURN(true); + + WHEN("Steam is constructed") + THEN("Exception is not thrown") + CHECK_NOTHROW(Steam steam({default_steam_path})); + } +} + + +TEST_CASE_FIXTURE(SteamTests, "Constructor_Failed_InvalidPaths") +{ + GIVEN("Not existing Steam config file") + { + std::filesystem::path const non_existent_path = "/nowhere"; + std::filesystem::path const non_existent_config = non_existent_path / "config/config.vdf"; + + REQUIRE_CALL(filesystemUtilsMock, Exists(non_existent_config)).RETURN(false); + WHEN("Steam is constructed") + THEN("Exception is thrown") + CHECK_THROWS_AS(Steam(std::vector{non_existent_path}), SteamInstallNotFoundException); + } +} + +TEST_CASE_FIXTURE(SteamTests, "FindInstallPaths_Success_WithoutCustomLibraries") +{ + GIVEN("Valid Steam config file not containing any custom libraries [game install directories]") + { + std::vector expected_paths{default_steam_path}; + + REQUIRE_CALL(filesystemUtilsMock, Exists(default_config_path)).RETURN(true); + REQUIRE_CALL(stdUtilsMock, FileReadAllText(default_config_path)).LR_RETURN(empty_file_content); + REQUIRE_CALL(vdfMock, LoadFromText(empty_file_content, false, _)); + REQUIRE_CALL(vdfMock, GetValuesWithFilter("BaseInstallFolder", _)).RETURN(std::vector{}); + + Steam steam({default_steam_path}); + WHEN("GetInstallPaths is called") + THEN("Only main SteamLibrary is returned") + CHECK_EQ(expected_paths, steam.GetInstallPaths()); + } +} + +TEST_CASE_FIXTURE(SteamTests, "FindInstallPaths_Success_WithCustomLibraries") +{ + GIVEN("Valid Steam config file with two custom libraries") + { + std::vector expected_paths{default_steam_path, "/mnt/games/SteamLibrary", "/mnt/disk2/steamgames"}; + + REQUIRE_CALL(filesystemUtilsMock, Exists(default_config_path)).RETURN(true); + REQUIRE_CALL(stdUtilsMock, FileReadAllText(default_config_path)).LR_RETURN(empty_file_content); + REQUIRE_CALL(vdfMock, LoadFromText(empty_file_content, false, _)); + REQUIRE_CALL(vdfMock, GetValuesWithFilter("BaseInstallFolder", _)).LR_RETURN(steam_library_dirs); + + Steam steam({default_steam_path}); + WHEN("GetInstallPaths is called") + THEN("Main SteamLibrary and two custom libraries are returned") + CHECK_EQ(expected_paths, steam.GetInstallPaths()); + } +} + +TEST_CASE_FIXTURE(SteamTests, "GetGamePathFromInstallPath_Success") +{ + GIVEN("Valid appmanifest for Arma 3 game") + { + std::filesystem::path const arma3_path = default_steam_path / "steamapps/common/Arma 3"; + std::filesystem::path const arma3_manifest_path = default_steam_path / "steamapps/appmanifest_107410.acf"; + + REQUIRE_CALL(filesystemUtilsMock, Exists(default_config_path)).RETURN(true); + REQUIRE_CALL(stdUtilsMock, FileReadAllText(arma3_manifest_path)).LR_RETURN(empty_file_content); + REQUIRE_CALL(vdfMock, LoadFromText(empty_file_content, false, _)).SIDE_EFFECT(_3.KeyValue["AppState/installdir"] = "Arma 3"); + + Steam steam({default_steam_path}); + WHEN("Getting install path of Arma 3") + THEN("Arma 3 path is returned") + CHECK_EQ(arma3_path, steam.GetGamePathFromInstallPath(default_steam_path, arma3_workshop_id)); + } +} + +TEST_CASE_FIXTURE(SteamTests, "GetWorkshopDir_Success") +{ + GIVEN("Valid appid for installed game") + { + std::string const invalid_game = "107411"; + std::filesystem::path const expected_workshop_path = default_steam_path / "steamapps/workshop/content" / arma3_workshop_id; + std::filesystem::path const not_existing_workshop_path = default_steam_path / "steamapps/workshop/content" / invalid_game; + + REQUIRE_CALL(filesystemUtilsMock, Exists(default_config_path)).RETURN(true); + REQUIRE_CALL(filesystemUtilsMock, Exists(expected_workshop_path)).RETURN(true); + REQUIRE_CALL(filesystemUtilsMock, Exists(not_existing_workshop_path)).RETURN(false); + + Steam steam({default_steam_path}); + + WHEN("Getting workshop path for installed game") + THEN("Workshop path for installed game is returned") + CHECK_EQ(expected_workshop_path, steam.GetWorkshopPath(default_steam_path, arma3_workshop_id)); + CHECK_THROWS_AS(steam.GetWorkshopPath(default_steam_path, invalid_game), SteamWorkshopDirectoryNotFoundException); + } +} + +TEST_CASE_FIXTURE(SteamTests, "GetWorkshopDir_Failed_NotExistingApp") +{ + GIVEN("Valid appid for installed game") + { + std::string const invalid_game = "107411"; + std::filesystem::path const not_existing_workshop_path = default_steam_path / "steamapps/workshop/content" / invalid_game; + + REQUIRE_CALL(filesystemUtilsMock, Exists(default_config_path)).RETURN(true); + REQUIRE_CALL(filesystemUtilsMock, Exists(not_existing_workshop_path)).RETURN(false); + + Steam steam({default_steam_path}); + + WHEN("Getting workshop path for not installed game") + THEN("Exception is thrown") + CHECK_THROWS_AS(steam.GetWorkshopPath(default_steam_path, invalid_game), SteamWorkshopDirectoryNotFoundException); + } +} diff --git a/tests/arma3-unix-launcher-library/test_string_utils.cpp b/tests/arma3-unix-launcher-library/test_string_utils.cpp new file mode 100644 index 0000000..3dd1017 --- /dev/null +++ b/tests/arma3-unix-launcher-library/test_string_utils.cpp @@ -0,0 +1,163 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include + +#include "default_test_reporter.hpp" + +#include "string_utils.hpp" + +TEST_CASE("RemoveElementsFromPath") +{ + using namespace StringUtils; + std::string const path = "/mnt/folder1/folder2/folder3"; + CHECK_EQ(RemoveElementsFromPath(path), "/mnt/folder1/folder2"); + CHECK_EQ(RemoveElementsFromPath(path, false), "/mnt/folder1/folder2/"); + CHECK_EQ(RemoveElementsFromPath(path, true, 2), "/mnt/folder1"); + CHECK_EQ(RemoveElementsFromPath(path, false, 2), "/mnt/folder1/"); + CHECK_EQ(RemoveElementsFromPath(path, false, 4), "/"); + CHECK_EQ(RemoveElementsFromPath(path, true, 4), ""); + + CHECK_EQ(RemoveElementsFromPath(path, false, 32), "/"); + CHECK_EQ(RemoveElementsFromPath(path, true, 32), ""); + + CHECK_EQ(RemoveElementsFromPath("/"), ""); + CHECK_EQ(RemoveElementsFromPath("thisisnotapath"), "thisisnotapath"); + + CHECK_EQ(RemoveElementsFromPath(""), ""); +} + +TEST_CASE("Replace") +{ + using namespace StringUtils; + CHECK_EQ(Replace("aaaa", "aa", "a"), "aa"); + CHECK_EQ(Replace("aaba", "aa", "a"), "aba"); + CHECK_EQ(Replace("aaaaa", "aa", "a"), "aaa"); + CHECK_EQ(Replace("this is a text", " a", ""), "this is text"); + + CHECK_EQ(Replace("/dir1/dir2/", "/", "\\"), "\\dir1\\dir2\\"); + + // Shouldn't be able to find empty string + CHECK_EQ(Replace("testing", "", "test"), "testing"); + + CHECK_EQ(Replace("tests", "te", "ab"), "absts"); + CHECK_EQ(Replace("tests", "testss", "ab"), "tests"); + CHECK_EQ(Replace("tests", "tests", "ab"), "ab"); + + CHECK_EQ(Replace("first line\nsecond line\nthird line", "line", "l"), "first l\nsecond l\nthird l"); + CHECK_EQ(Replace("whole\tstring", "\t", "\n"), "whole\nstring"); +} + +TEST_CASE("Trim") +{ + using namespace StringUtils; + const std::string to_trim = "\n\n\n\n\n\n test string\t\n \t\t "; + const std::string trimmed_left = "test string\t\n \t\t "; + const std::string trimmed_right = "\n\n\n\n\n\n test string"; + const std::string trimmed = "test string"; + + CHECK_EQ(trim(to_trim), trimmed); + CHECK_EQ(trim_left(to_trim), trimmed_left); + CHECK_EQ(trim_right(to_trim), trimmed_right); + + CHECK_EQ(trim(trimmed), trimmed); + + auto trim_with_copy = trim(to_trim); + CHECK_EQ(trim_with_copy, trimmed); + + std::string_view trim_with_view_argument(to_trim); + auto trimmed_view = trim(trim_with_view_argument); + CHECK_EQ(trimmed_view, trimmed); +} + +TEST_CASE("StartsWith") +{ + using namespace StringUtils; + std::string const text = "This is some text"; + + CHECK(StartsWith(text, "This")); + CHECK(StartsWith(text, "This ")); + CHECK(StartsWith(text, "This is some text")); + + CHECK_FALSE(StartsWith(text, "This is some text ")); + CHECK_FALSE(StartsWith(text, "this")); + CHECK_FALSE(StartsWith(text, "\nThis")); + CHECK_FALSE(StartsWith(text, " This")); + CHECK_FALSE(StartsWith(text, "text")); +} + +TEST_CASE("EndsWith") +{ + using namespace StringUtils; + std::string const text = "This is some text"; + + CHECK(EndsWith(text, "text")); + CHECK(EndsWith(text, " text")); + CHECK(EndsWith(text, "This is some text")); + CHECK_FALSE(EndsWith(text, "this is some text")); + CHECK_FALSE(EndsWith(text, " This is some text")); + CHECK_FALSE(EndsWith(text, "\ntext")); +} + +TEST_CASE("Split") +{ + using namespace StringUtils; + std::string const text = "This is some text"; + + std::vector text_split_by_space + { + std::string_view(text.c_str(), 4), + std::string_view(text.c_str() + 5, 2), + std::string_view(text.c_str() + 8, 4), + std::string_view(text.c_str() + 13, 4) + }; + + CHECK_EQ(text_split_by_space, Split(text, " ")); + + std::vector text_split_by_t + { + std::string_view(text.c_str() + 1, 12), + std::string_view(text.c_str() + 14, 2), + }; + + CHECK_EQ(text_split_by_t, Split(text, "Tt")); + + CHECK_EQ(std::vector(), Split(" ", " ")); + + std::string const remove_stamina_text = "name=\"Remove Stamina\""; + std::vector remove_stamina + { + std::string_view(remove_stamina_text.c_str(), 4), + std::string_view(remove_stamina_text.c_str() + 5, 16) + }; + CHECK_EQ(remove_stamina, Split(remove_stamina_text, "=")); +} + +TEST_CASE("ToWindowsPath") +{ + using namespace std::filesystem; + using namespace StringUtils; + + SUBCASE("Relative path") + { + path linux_path{"dir1/dir2/file.cpp"}; + path windows_path{R"(dir1\dir2\file.cpp)"}; + CHECK_EQ(windows_path, ToWindowsPath(linux_path)); + } + + SUBCASE("Absolute path") + { + path linux_path{"/dir1/dir2/file.cpp"}; + + SUBCASE("Default drive") + { + path windows_path{R"(C:\dir1\dir2\file.cpp)"}; + CHECK_EQ(windows_path, ToWindowsPath(linux_path)); + } + + SUBCASE("Custom drive") + { + path windows_path{R"(Z:\dir1\dir2\file.cpp)"}; + CHECK_EQ(windows_path, ToWindowsPath(linux_path, 'Z')); + } + } +} diff --git a/tests/arma3-unix-launcher-library/test_vdf.cpp b/tests/arma3-unix-launcher-library/test_vdf.cpp new file mode 100644 index 0000000..0f75917 --- /dev/null +++ b/tests/arma3-unix-launcher-library/test_vdf.cpp @@ -0,0 +1,217 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include "default_test_reporter.hpp" + +#include "vdf.hpp" + +constexpr std::string_view vdf_valid = R"vdf( +"VDFTests" +{ + "Branch1" + { + "SubBranch1" + { + "Application1" + { + "SomeKey" "5" + "SomeKeyWithValues" + { + "KeyValue" "text" + "KeyValueInt" "155" + } + } + } + "SubBranch2" + { + "KeyValue" "8" + } + } + "Branch2" + { + "SubBranch2" + { + "Directories" + { + "BaseInstallFolder_1" "/mnt/games/SteamLibrary" + "BaseInstallFolder_2" "/home/user/SteamLibrary" + "BaseInstallFolder_3" "/run/media/user/SteamLibrary" + "BaseInstallFolder_4" "/somerandompath/steamlibrary" + } + } + } +} +)vdf"; + +constexpr std::string_view vdf_valid_mixed_spaces_with_tabs = R"vdf( +"VDFTests" +{ + "Branch1" + { + "SubBranch1" + { + "Application1" + { + "SomeKey" "5" + "SomeKeyWithValues" + { + "KeyValue" "text" + "KeyValueInt" "155" + } + } + } + "SubBranch2" +{ + "KeyValue" "8" +} + } +"Branch2" +{ + "SubBranch2" + { + "Directories" + { + "BaseInstallFolder_1" "/mnt/games/SteamLibrary" + "BaseInstallFolder_2" "/home/user/SteamLibrary" + "BaseInstallFolder_3" "/run/media/user/SteamLibrary" + "BaseInstallFolder_4" "/somerandompath/steamlibrary" + } + } + } +} +)vdf"; + +constexpr std::string_view vdf_invalid_missing_brackets = R"vdf( +"VDFTests" +{ + "Branch1" + { + "SubBranch1" + "Application1" + "SomeKey" "5" + "SomeKeyWithValues" + { + "KeyValue" "text" + "KeyValueInt" "155" + } + } + } + "SubBranch2" + "KeyValue" "8" + "Branch2" + { + "SubBranch2" + { + "Directories" + { + "BaseInstallFolder_1" "/mnt/games/SteamLibrary" + "BaseInstallFolder_2" "/home/user/SteamLibrary" + "BaseInstallFolder_3" "/run/media/user/SteamLibrary" + "BaseInstallFolder_4" "/somerandompath/steamlibrary" + } +)vdf"; + +TEST_CASE("BasicFilter") +{ + GIVEN("VDF filled with KeyX/ValueX pairs") + { + VDF vdf; + std::string key = "Key"; + std::string value = "Value"; + for (int i = 0; i < 10; i++) + { + std::string number = std::to_string(i + 1); + vdf.KeyValue[key + number] = value + number; + } + WHEN("Filtering VDF with non-existing filter") + { + THEN("Result should be empty (zero size)") + { + CHECK_EQ(static_cast(0), vdf.GetValuesWithFilter("This should be empty").size()); + } + } + + WHEN("Filtering VDF with filter matching all keys") + { + THEN("Result shold contain all values") + { + CHECK_EQ(vdf.KeyValue.size(), vdf.GetValuesWithFilter("Key").size()); + } + } + + WHEN("Filtering VDF with filter matching two keys") + { + THEN("Result should contain two keys") + { + CHECK_EQ(static_cast(2), vdf.GetValuesWithFilter("Key1").size()); + } + } + } +} + +TEST_CASE("BasicParser") +{ + GIVEN("Empty VDF") + { + VDF vdf; + WHEN("\"Key\" \"Value\"") + { + std::string simple_key_value = R"vdf("Key""Value")vdf"; + vdf.LoadFromText(simple_key_value); + THEN("Key points to Value") + { + CHECK_EQ("Value", vdf.KeyValue["Key"]); + CHECK_EQ(static_cast(1), vdf.KeyValue.size()); + } + } + + WHEN("\"Branch\" { \"Key\" \"Value\" }") + { + std::string simple_key_value = R"vdf("Branch"{"Key""Value"})vdf"; + vdf.LoadFromText(simple_key_value); + THEN("Branch\\Key points to Value") + { + CHECK_EQ("Value", vdf.KeyValue["Branch/Key"]); + CHECK_EQ(static_cast(1), vdf.KeyValue.size()); + } + } + } +} + +TEST_CASE("LoadFromFile") +{ + GIVEN("Two empty VDFs") + { + VDF vdf, vdfWithTabs; + WHEN("load VDF 1 with valid file, load VDF 2 with file using mixed spaces and tabs") + { + vdf.LoadFromText(vdf_valid); + vdfWithTabs.LoadFromText(vdf_valid_mixed_spaces_with_tabs); + THEN("Both VDFs should be equal") + { + CHECK_EQ(vdf.KeyValue, vdfWithTabs.KeyValue); + CHECK_EQ(static_cast(8), vdf.KeyValue.size()); + } + } + } +} + +TEST_CASE("ParserThenFilter") +{ + GIVEN("Valid Steam VDF with various key-value pairs, list of valid paths") + { + VDF vdf; + vdf.LoadFromText(vdf_valid); + std::vector paths{"/mnt/games/SteamLibrary", "/home/user/SteamLibrary", "/run/media/user/SteamLibrary", "/somerandompath/steamlibrary"}; + + WHEN("Filtering valid VDF by BaseInstallFolder and sorting output") + { + std::vector filtered = vdf.GetValuesWithFilter("BaseInstallFolder"); + std::sort(filtered.begin(), filtered.end()); + std::sort(paths.begin(), paths.end()); + THEN("Sorted output should be equal to paths") + { + CHECK_EQ(filtered, paths); + } + } + } +} diff --git a/tests/mock/cppfilter.hpp b/tests/mock/cppfilter.hpp new file mode 100644 index 0000000..f0e24bf --- /dev/null +++ b/tests/mock/cppfilter.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +class CppFilter; + +class CppFilterMock +{ +public: + CppFilterMock() { instance = this; } + ~CppFilterMock() { instance = nullptr; } + static inline CppFilterMock* instance; + + MAKE_MOCK2(RemoveClass, std::string(std::string const &, CppFilter&)); +}; + +/*#define MAKE_STUBOMOCK(ret_type, class_name, method_name, args, mock_type) \ + ret_type class_name::method_name(args) \ + { \ + return mocktype::instance->method_name(args, *this); \ + } + +MAKE_STUBOMOCK(std::string, CppFilter, RemoveClass, (std::string const&), CppFilterMock);*/ + +std::string CppFilter::RemoveClass(std::string const& class_name) +{ + return CppFilterMock::instance->RemoveClass(class_name, *this); +} + diff --git a/tests/mock/filesystem_utils.hpp b/tests/mock/filesystem_utils.hpp new file mode 100644 index 0000000..f7978e7 --- /dev/null +++ b/tests/mock/filesystem_utils.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +class FilesystemUtilsMock +{ +public: + FilesystemUtilsMock() { instance = this; } + ~FilesystemUtilsMock() { instance = nullptr; } + static inline FilesystemUtilsMock* instance; + + MAKE_MOCK1(CreateDirectories, bool(std::filesystem::path const&)); + MAKE_MOCK1(Exists, bool(std::filesystem::path const&)); + MAKE_MOCK1(IsDirectory, bool(std::filesystem::path const&)); + MAKE_MOCK2(Ls, std::vector(std::filesystem::path const&, bool)); +}; + +namespace FilesystemUtils +{ + bool CreateDirectories(std::filesystem::path const &path) + { + return FilesystemUtilsMock::instance->CreateDirectories(path); + } + + bool Exists(std::filesystem::path const& path) + { + return FilesystemUtilsMock::instance->Exists(path); + } + + bool IsDirectory(std::filesystem::path const& path) + { + return FilesystemUtilsMock::instance->IsDirectory(path); + } + + std::vector Ls(std::filesystem::path const &path, bool set_lowercase) + { + return FilesystemUtilsMock::instance->Ls(path, set_lowercase); + } +} diff --git a/tests/mock/mock_tracer.hpp b/tests/mock/mock_tracer.hpp new file mode 100644 index 0000000..831a40b --- /dev/null +++ b/tests/mock/mock_tracer.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include +#include + +static inline trompeloeil::stream_tracer tracer{std::cout}; diff --git a/tests/mock/mod.hpp b/tests/mock/mod.hpp new file mode 100644 index 0000000..c6200f9 --- /dev/null +++ b/tests/mock/mod.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +class Mod; + +class ModMock +{ +public: + ModMock() { instance = this; } + ~ModMock() { instance = nullptr; } + static inline ModMock* instance; + + MAKE_MOCK2(Constructor, void(std::filesystem::path const&, Mod&)); + MAKE_MOCK1(GetName, std::string(Mod&)); + MAKE_MOCK1(LoadAllCPP, void(Mod&)); + MAKE_MOCK3(LoadFromText, void(std::string const&, bool, Mod&)); +}; + +Mod::Mod(std::filesystem::path const& path) : path_(path) +{ + ModMock::instance->Constructor(path, *this); +} + +std::string Mod::GetName() +{ + return ModMock::instance->GetName(*this); +} + +void Mod::LoadAllCPP() +{ + ModMock::instance->LoadAllCPP(*this); +} + +void Mod::LoadFromText(std::string const &text, bool append) +{ + ModMock::instance->LoadFromText(text, append, *this); +} diff --git a/tests/mock/std_utils.hpp b/tests/mock/std_utils.hpp new file mode 100644 index 0000000..871959c --- /dev/null +++ b/tests/mock/std_utils.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +class StdUtilsMock +{ +public: + StdUtilsMock() { instance = this; } + ~StdUtilsMock() { instance = nullptr; } + static inline StdUtilsMock* instance; + + MAKE_MOCK1(CreateFile, bool(std::filesystem::path const&)); + MAKE_MOCK1(FileReadAllText, std::string(std::filesystem::path const&)); + MAKE_MOCK2(FileWriteAllText, void(std::filesystem::path const&, std::string const&)); + MAKE_MOCK1(IsProcessRunning, pid_t(std::string const&)); + MAKE_MOCK1(StartBackgroundProcess, void(std::string const&)); + MAKE_MOCK1(GetConfigFilePath, std::filesystem::path(std::filesystem::path const&)); +}; + +namespace StdUtils +{ + bool CreateFile(std::filesystem::path const &path) + { + return StdUtilsMock::instance->CreateFile(path); + } + + std::string FileReadAllText(std::filesystem::path const &path) + { + return StdUtilsMock::instance->FileReadAllText(path); + } + + void FileWriteAllText(std::filesystem::path const &path, std::string const &text) + { + StdUtilsMock::instance->FileWriteAllText(path, text); + } + + pid_t IsProcessRunning(std::string const &name) + { + return StdUtilsMock::instance->IsProcessRunning(name); + } + + void StartBackgroundProcess(std::string const &command) + { + StdUtilsMock::instance->StartBackgroundProcess(command); + } + + std::filesystem::path GetConfigFilePath(std::filesystem::path const &config_filename) + { + return StdUtilsMock::instance->GetConfigFilePath(config_filename); + } +} diff --git a/tests/mock/vdf.hpp b/tests/mock/vdf.hpp new file mode 100644 index 0000000..e72c8d5 --- /dev/null +++ b/tests/mock/vdf.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +class VDF; + +class VdfMock +{ +public: + VdfMock() { instance = this; } + ~VdfMock() { instance = nullptr; } + static inline VdfMock* instance; + + MAKE_MOCK3(LoadFromText, void(std::string_view const, bool, VDF&)); + MAKE_MOCK2(GetValuesWithFilter, std::vector(std::string_view const, VDF&)); +}; + +std::vector VDF::GetValuesWithFilter(std::string_view const filter) +{ + return VdfMock::instance->GetValuesWithFilter(filter, *this); +} + +void VDF::LoadFromText(std::string_view const text, bool append) +{ + VdfMock::instance->LoadFromText(text, append, *this); +} diff --git a/tools/ci/build-images.sh b/tools/ci/build-images.sh new file mode 100755 index 0000000..6fba8e0 --- /dev/null +++ b/tools/ci/build-images.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +SELF_PATH=`dirname "$(readlink -f $0)"` +pushd $SELF_PATH + +#no cache because mirrors get invalidated quickly +docker build --no-cache -t a3ul_archlinux_build -f docker/Dockerfile.a3ul_archlinux_build . +docker build --no-cache -t a3ul_ubuntu-18.04_build -f docker/Dockerfile.a3ul_ubuntu-18.04_build . + +docker build --no-cache -t github-release -f docker/Dockerfile.github-release . + +popd diff --git a/tools/ci/build-package.sh b/tools/ci/build-package.sh new file mode 100755 index 0000000..4c06847 --- /dev/null +++ b/tools/ci/build-package.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash + +SELF_PATH=`dirname "$(readlink -f $0)"` +pushd $SELF_PATH + +print_help() +{ +printf "Usage: $0 [OPTION]... +Build Arma 3 Unix Launcher for given OS + +Available options + -b, --build-target build for given target, use output from '--list-systems' + -l, --list-systems List available systems to build for + -u, --use-local-image do not use images from muttleyxd/ on DockerHub, instead use local ones (build & run) +" + exit 0 +} + +log_and_exit() +{ + echo "Error: $@" + exit 1 +} + +list_targets() +{ + ls docker/Dockerfile.a3ul_*_build | awk '{split($0, a, "_"); print a[2]}' + exit 0 +} + +TEMP=`getopt -o b:,h,l,u --long build-target:,help,list-systems,use-local-image -n 'test.sh' -- "$@"` +eval set -- "$TEMP" + +BUILD_TARGET="" +USE_LOCAL_IMAGE=0 + +while true ; do + case "$1" in + -b|--build-targets) + case "$2" in + "") print_help;; + *) BUILD_TARGET=$2 ; shift 2 ;; + esac ;; + -h|--help) print_help $0 ; shift ;; + -l|--list-targets) + list_targets $0 ; shift ;; + -u|--use-local-image) + USE_LOCAL_IMAGE=1 ; shift ;; + --) shift ; break ;; + *) log_and_exit "Invalid argument $1" ;; + esac +done + +if [ "$BUILD_TARGET" == "" ]; then + print_help +else + CONTAINER_NAME=`echo "a3ul_" "$BUILD_TARGET" "_build" | awk '{print $1$2$3}'` + DOCKERFILE_EXISTS=`ls docker/Dockerfile.$CONTAINER_NAME 2>&1` + if [ $? -ne 0 ]; then + log_and_exit "$DOCKERFILE_EXISTS" + fi +fi + +echo "container name: $CONTAINER_NAME" + +if [ "$USE_LOCAL_IMAGE" -eq 0 ]; then + IMAGE_TO_USE="muttleyxd/$CONTAINER_NAME" + echo "Fetching $IMAGE_TO_USE from DockerHub" + docker pull $IMAGE_TO_USE +else + IMAGE_TO_USE="$CONTAINER_NAME" + echo "Building $IMAGE_TO_USE" + docker build -t $IMAGE_TO_USE -f docker/Dockerfile.$CONTAINER_NAME docker +fi + +COMMAND_SET="cd /build && ./build.sh"; +docker run -it -v "$SELF_PATH"/packaging/$BUILD_TARGET:/build -v "$SELF_PATH"/../..:/arma3-unix-launcher --rm $IMAGE_TO_USE bash -c "$COMMAND_SET" +if [ "$?" -eq 0 ]; then + echo "Build successful, package available at: " + find . -mmin -1 -type f -exec realpath {} + +fi + +popd diff --git a/tools/ci/docker/Dockerfile.a3ul_archlinux_build b/tools/ci/docker/Dockerfile.a3ul_archlinux_build new file mode 100644 index 0000000..67d70f4 --- /dev/null +++ b/tools/ci/docker/Dockerfile.a3ul_archlinux_build @@ -0,0 +1,20 @@ +FROM archlinux/base + +# Update image +RUN pacman -Syu --noconfirm + +# Install required dependencies +RUN pacman -S --noconfirm base-devel cmake make gcc git qt5-base qt5-svg fmt + +# Cleanup +RUN rm -rf /var/cache/pacman/pkg/* + +# Download nlohmann-json and doctest from GitHub +ADD https://github.com/nlohmann/json/releases/download/v3.7.3/json.hpp /usr/include/nlohmann/json.hpp +ADD https://raw.githubusercontent.com/onqtam/doctest/2.3.6/doctest/doctest.h /usr/include/doctest/doctest.h +RUN chmod 644 /usr/include/nlohmann/json.hpp /usr/include/doctest/doctest.h + +# We cannot run makepkg as root, so we need a builduser +RUN useradd -m builduser && passwd -d builduser + +USER builduser diff --git a/tools/ci/docker/Dockerfile.a3ul_ubuntu-18.04_build b/tools/ci/docker/Dockerfile.a3ul_ubuntu-18.04_build new file mode 100644 index 0000000..0a2e664 --- /dev/null +++ b/tools/ci/docker/Dockerfile.a3ul_ubuntu-18.04_build @@ -0,0 +1,25 @@ +FROM ubuntu:18.04 + +# Update image +RUN apt-get update && apt-get upgrade -y + +# Enable CMake PPA +RUN apt-get install -y apt-transport-https ca-certificates gnupg software-properties-common wget +RUN wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | apt-key add - +RUN apt-add-repository 'deb https://apt.kitware.com/ubuntu/ bionic main' && apt-get update + +# Install required dependencies +RUN apt-get install -y build-essential devscripts cmake g++-8 qt5-default libqt5widgets5 libqt5svg5 libqt5svg5-dev libfmt-dev + +# Download nlohmann-json and doctest from GitHub +ADD https://github.com/nlohmann/json/releases/download/v3.7.3/json.hpp /usr/include/nlohmann/json.hpp +ADD https://raw.githubusercontent.com/onqtam/doctest/2.3.6/doctest/doctest.h /usr/include/doctest/doctest.h +RUN chmod 644 /usr/include/nlohmann/json.hpp /usr/include/doctest/doctest.h + +# Cleanup +RUN rm -rf /var/lib/apt/lists/* + +# Let's not run as root +RUN useradd -m builduser && passwd -d builduser + +USER builduser diff --git a/tools/ci/docker/Dockerfile.github-release b/tools/ci/docker/Dockerfile.github-release new file mode 100644 index 0000000..c7a36d2 --- /dev/null +++ b/tools/ci/docker/Dockerfile.github-release @@ -0,0 +1,8 @@ +FROM alpine:edge + +# Update image +RUN apk update && apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/testing py3-pygithub + +# Install github-release.py +ADD https://raw.githubusercontent.com/muttleyxd/github-release/master/github-release.py /usr/bin/github-release +RUN chmod 755 /usr/bin/github-release diff --git a/tools/ci/packaging/archlinux/PKGBUILD b/tools/ci/packaging/archlinux/PKGBUILD new file mode 100644 index 0000000..34a0c08 --- /dev/null +++ b/tools/ci/packaging/archlinux/PKGBUILD @@ -0,0 +1,43 @@ +# Maintainer: muttleyxd +pkgname=arma3-unix-launcher +pkgver=1 +pkgrel=1 +pkgdesc="Launcher for ArmA 3 on Linux" +arch=('x86_64') +url="https://github.com/muttleyxd/arma3-unix-launcher" +license=('MIT') +depends=('fmt' 'qt5-base' 'qt5-svg') +makedepends=('cmake' 'make') +provides=('arma3-unix-launcher') +conflicts=('arma3-unix-launcher') +source=("git+file:///arma3-unix-launcher") +md5sums=('SKIP') + +pkgver() +{ + cd "/arma3-unix-launcher" + echo $(git rev-list --count HEAD).$(git rev-parse --short HEAD) +} + +build() +{ + cd "$srcdir/$pkgname" + mkdir build + cd build + cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DRUN_TESTS=ON + make +} + +check() +{ + cd "$srcdir/$pkgname/build" + ctest -V +} + +package() +{ + cd "$srcdir/$pkgname" + install -D -m644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" + cd "$srcdir/$pkgname/build" + make DESTDIR="$pkgdir" install +} diff --git a/tools/ci/packaging/archlinux/build.sh b/tools/ci/packaging/archlinux/build.sh new file mode 100755 index 0000000..75c1b0b --- /dev/null +++ b/tools/ci/packaging/archlinux/build.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euxo pipefail + +mkdir /tmp/build +cd /tmp/build +cp /build/PKGBUILD ./ +makepkg +cp /tmp/build/*.tar.xz /build diff --git a/tools/ci/packaging/mac-os-x/build.sh b/tools/ci/packaging/mac-os-x/build.sh new file mode 100755 index 0000000..72d5216 --- /dev/null +++ b/tools/ci/packaging/mac-os-x/build.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euxo pipefail + +realpath() { + [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" +} + +SELF_PATH=`realpath $(dirname $0)` +A3UL_PATH=`realpath $SELF_PATH/../../../..` + +BUILD_DIR=/tmp/build_a3ul_mac + +mkdir -p $BUILD_DIR +pushd $BUILD_DIR + +cmake $A3UL_PATH -DCPACK_GENERATOR=DragNDrop -DCMAKE_CXX_COMPILER=g++-9 -DCMAKE_PREFIX_PATH='/usr/local;/usr/local/opt/qt' +make -j4 +make package + +popd diff --git a/tools/ci/packaging/ubuntu-18.04/build.sh b/tools/ci/packaging/ubuntu-18.04/build.sh new file mode 100755 index 0000000..260354b --- /dev/null +++ b/tools/ci/packaging/ubuntu-18.04/build.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euxo pipefail + +TMP_BUILD_DIR="/tmp/build" + +source /etc/lsb-release +source /etc/os-release + +pushd /arma3-unix-launcher + SHORT_HASH=`git rev-parse --verify HEAD | cut -c -7` + BRANCH=`git branch | grep "*" | cut -c 3-` + COMMIT_COUNT=`git rev-list HEAD --count` +popd + +PKG_DIR="$TMP_BUILD_DIR/arma3-unix-launcher_$BRANCH-$COMMIT_COUNT-$SHORT_HASH-$ID-$VERSION_ID-amd64" +mkdir -p $PKG_DIR/DEBIAN + +pushd $PKG_DIR + sed 's/VERSION/$COMMIT_COUNT-$SHORT_HASH/g' /build/control >./DEBIAN/control +popd + +pushd $TMP_BUILD_DIR + mkdir cmake_build + + pushd cmake_build + cmake /arma3-unix-launcher -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_INSTALL_PREFIX=/usr -DRUN_TESTS=ON + make -j4 + ctest --output-on-failure + make install DESTDIR=$PKG_DIR + popd + + dpkg-deb --build $PKG_DIR + cp *.deb /build +popd diff --git a/tools/ci/packaging/ubuntu-18.04/control b/tools/ci/packaging/ubuntu-18.04/control new file mode 100644 index 0000000..8d9d8f0 --- /dev/null +++ b/tools/ci/packaging/ubuntu-18.04/control @@ -0,0 +1,9 @@ +Package: arma3-unix-launcher +Version: 0.1 +Architecture: amd64 +Maintainer: muttleyxd +Depends: libqt5widgets5, libqt5svg5 +Section: testing +Priority: optional +Homepage: https://github.com/muttleyxd/arma3-unix-launcher.git +Description: Advanced launcher Linux and Mac ArmA 3