Browse Source

Packaging: data directory lookup, Ogre plugin search, install rules and CPack

The engine can now be installed and packaged: it no longer has to be started from the data directory, the Ogre plugins come from the system instead of the repository, and `cpack` makes a .deb and a .tar.gz.

Engine:
- The engine finds its data directory: --data-dir, VGEARS_DATA_DIR, the current directory if it has the V-Gears files, or ~/.v-gears (%USERPROFILE%\v-gears in Windows, the installer's default). It changes into it, so the relative paths keep working. Relative paths given in the command line are made absolute first.
- If the data directory has no V-Gears files, the missing ones are copied from the template: the repository's data/ folder, <prefix>/share/v-gears, a data/ folder next to the executable, or VGEARS_TEMPLATE_DIR. Existing files are never overwritten, and a non-empty unrelated directory is not filled (common/DataDirectory).
- The engine loads the plugins of plugins.cfg itself, looking in PluginFolder, the data directory, the executable's directory (and ../lib/OGRE), the Ogre plugin directory found at build time (VGEARS_OGRE_PLUGIN_DIR) and the usual system directories. In Linux only the plugin of the running Ogre version is accepted (common/OgrePlugins).
- A plugin that fails to load no longer crashes on exit: its exception pointed to the unloaded library. The error is printed instead, and a missing plugin lists the directories searched.

Installer:
- Finds the V-Gears data files with the same search as the engine, and Launch game passes --data-dir.
- No longer copies the system's Ogre plugins to the installation directory; the preflight check uses the engine's search to tell if they can be found.

Build and packaging:
- Install rules for the data template, documentation, desktop files and icons. Linux: bin/, share/v-gears/, share/doc/v-gears/. Windows and macOS: a portable folder with the template in data/ (CMake/InstallDirs.cmake).
- CPack: .deb (dependencies from the linked libraries, recommends ffmpeg and timidity) and .tar.gz on Linux, .zip on Windows and macOS (CMake/Packaging.cmake). The game is installed to bin/ instead of games/.
- Boost is found through its CMake config files when the earlier search finds nothing. The if (MINGW) block in src/CMakeLists.txt is not closed where it should be and skips the library detection on Linux; that's not fixed yet, so USE_DETERMINISTIC_FALLBACKS must stay ON.
- Removed debian/ (the CPack .deb replaces it) and data/RenderSystem_GL.so.1.12.10. PluginFolder is no longer set in plugins.cfg and plugins_d.cfg.
- v-gears.desktop runs v-gears directly, without the cd ~/.v-gears wrapper.

Tests
- DataDirectory and OgrePlugins tests, added to v-gears-installer-tests.

Docs
- RUN, INSTALL and BUILD updated: the data directory lookup, and how to build the packages.
Iñigo Valentin 4 hours ago
parent
commit
7c4f7dd985

+ 27 - 0
CMake/InstallDirs.cmake

@@ -0,0 +1,27 @@
+# Where `make install` (and the packages made with CPack) put each part of V-Gears.
+#
+# Linux and other Unix systems use the usual layout of the prefix:
+#   <prefix>/bin/v-gears, <prefix>/bin/v-gears-installer
+#   <prefix>/share/v-gears/          The data template: plugins.cfg, resources.cfg, data/...
+#   <prefix>/share/doc/v-gears/      Documentation and license
+#   <prefix>/share/applications/     Desktop files, and the icons in share/icons and share/pixmaps
+#
+# Windows and macOS use a single portable folder:
+#   <prefix>/v-gears(.exe), <prefix>/v-gears-installer(.exe)
+#   <prefix>/data/                   The data template
+#   <prefix>/                        Documentation and license
+#
+# The game finds the template in both layouts (see src/common/DataDirectory.h), and copies it to the data directory
+# of the user the first time it runs.
+
+include(GNUInstallDirs)
+
+if(WIN32 OR APPLE)
+    set(VGEARS_INSTALL_BINDIR ".")
+    set(VGEARS_INSTALL_TEMPLATEDIR "data")
+    set(VGEARS_INSTALL_DOCDIR ".")
+else()
+    set(VGEARS_INSTALL_BINDIR "${CMAKE_INSTALL_BINDIR}")
+    set(VGEARS_INSTALL_TEMPLATEDIR "${CMAKE_INSTALL_DATADIR}/v-gears")
+    set(VGEARS_INSTALL_DOCDIR "${CMAKE_INSTALL_DATAROOTDIR}/doc/v-gears")
+endif()

+ 91 - 0
CMake/Packaging.cmake

@@ -0,0 +1,91 @@
+# Install rules for everything but the executables (those are in src/), and the CPack configuration.
+#
+# The directories are set in InstallDirs.cmake. To make the packages, from the build directory:
+#   cmake --build . && cpack            Linux: .deb and .tar.gz. Windows and macOS: .zip.
+#   cpack -G TGZ                        Only one of them.
+# To install without a package:
+#   cmake --install . --prefix /usr/local
+
+# Data template. It's copied to the data directory of each user, where the data installer adds the game data.
+install(
+    DIRECTORY ${CMAKE_SOURCE_DIR}/data/
+    DESTINATION ${VGEARS_INSTALL_TEMPLATEDIR}
+    # Ogre plugins are found in the system or next to the executable, never in the template.
+    PATTERN "*.so" EXCLUDE
+    PATTERN "*.so.*" EXCLUDE
+    PATTERN "*.dll" EXCLUDE
+    PATTERN "*.dylib" EXCLUDE
+)
+
+# Documentation for players. The developer guides (build, style, code notes) are left out.
+install(
+    FILES
+        ${CMAKE_SOURCE_DIR}/README.md
+        ${CMAKE_SOURCE_DIR}/COPYING.txt
+        ${CMAKE_SOURCE_DIR}/doc/AUTHORS.md
+        ${CMAKE_SOURCE_DIR}/doc/HISTORY.md
+        ${CMAKE_SOURCE_DIR}/doc/INSTALL.md
+        ${CMAKE_SOURCE_DIR}/doc/INSTALL_DATA.md
+        ${CMAKE_SOURCE_DIR}/doc/RUN.md
+    DESTINATION ${VGEARS_INSTALL_DOCDIR}
+)
+
+# Desktop integration. The icon names must match the Icon key of the desktop files, and the installer's desktop file
+# name must match QApplication::setDesktopFileName so the shell shows the right icon for its window.
+if(UNIX AND NOT APPLE)
+    set(_vgears_desktop_files ${CMAKE_SOURCE_DIR}/v-gears.desktop)
+    set(_vgears_icons ${CMAKE_SOURCE_DIR}/v-gears.png)
+    if(BUILD_INSTALLER)
+        list(APPEND _vgears_desktop_files ${CMAKE_SOURCE_DIR}/v-gears-installer.desktop)
+        list(APPEND _vgears_icons ${CMAKE_SOURCE_DIR}/v-gears-installer.png)
+    endif()
+    install(FILES ${_vgears_desktop_files} DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications)
+    # TODO: The icons are 300x300. Add 256x256 versions so they match their hicolor folder.
+    install(FILES ${_vgears_icons} DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps)
+    install(FILES ${_vgears_icons} DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pixmaps)
+endif()
+
+# Packages.
+set(CPACK_PACKAGE_NAME "v-gears")
+set(CPACK_PACKAGE_VENDOR "The V-Gears Team")
+set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
+set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A free Final Fantasy VII engine")
+set(CPACK_PACKAGE_DESCRIPTION
+    "V-Gears is a free implementation of the Final Fantasy VII engine. It doesn't include any data of the original \
+game: the V-Gears installer extracts it from your own copy of the PC version.")
+set(CPACK_PACKAGE_HOMEPAGE_URL "https://v-gears.inigovalentin.com")
+set(CPACK_PACKAGE_INSTALL_DIRECTORY "V-Gears")
+set(CPACK_PACKAGE_EXECUTABLES "v-gears" "V-Gears" "v-gears-installer" "V-Gears Installer")
+set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/COPYING.txt")
+set(CPACK_RESOURCE_FILE_README "${CMAKE_SOURCE_DIR}/README.md")
+set(CPACK_STRIP_FILES ON)
+set(CPACK_PACKAGE_CHECKSUM SHA256)
+set(CPACK_PACKAGE_FILE_NAME "v-gears-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
+if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+    string(APPEND CPACK_PACKAGE_FILE_NAME "-debug")
+endif()
+
+if(WIN32 OR APPLE)
+    set(CPACK_GENERATOR "ZIP")
+    # The ZIP has a top-level folder, so it can be extracted anywhere.
+    set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY ON)
+else()
+    set(CPACK_GENERATOR "DEB;TGZ")
+    # The tarball has the prefix layout (bin/, share/...) in a top-level folder, so it can be extracted anywhere and
+    # run from there. The .deb installs to /usr, the default of the DEB generator.
+    set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY ON)
+endif()
+
+# Debian package.
+set(VGEARS_PACKAGE_MAINTAINER "Iñigo Valentin <i@inigovalentin.com>" CACHE STRING "Maintainer of the packages")
+set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${VGEARS_PACKAGE_MAINTAINER}")
+set(CPACK_DEBIAN_FILE_NAME "DEB-DEFAULT")
+set(CPACK_DEBIAN_PACKAGE_SECTION "games")
+set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
+# The dependencies are taken from the libraries the executables link. The Ogre plugins come in the same package as
+# libOgreMain.
+set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
+# The installer uses them to convert the music and the videos.
+set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "ffmpeg, timidity")
+
+include(CPack)

+ 5 - 18
CMakeLists.txt

@@ -78,6 +78,9 @@ else()
     set(VGEARS_LUAJIT_LIBRARIES ${VGEARS_LUAJIT_LIBRARY})
     set(VGEARS_LUAJIT_LIBRARIES ${VGEARS_LUAJIT_LIBRARY})
 endif()
 endif()
 
 
+# Where each part is installed. Set before src/, whose install rules use it.
+include(InstallDirs)
+
 # Build v-gears (and, if requested, the installer).
 # Build v-gears (and, if requested, the installer).
 add_subdirectory(src)
 add_subdirectory(src)
 
 
@@ -87,21 +90,5 @@ if(BUILD_TESTS)
     add_subdirectory(test)
     add_subdirectory(test)
 endif()
 endif()
 
 
-# Install Linux desktop integration assets for the installer so desktop shells
-# can map the running window to its icon.
-if(UNIX AND NOT APPLE AND BUILD_INSTALLER)
-    install(
-        FILES ${CMAKE_SOURCE_DIR}/v-gears-installer.desktop
-        DESTINATION share/applications
-    )
-    install(
-        FILES ${CMAKE_SOURCE_DIR}/v-gears-installer.png
-        DESTINATION share/icons/hicolor/256x256/apps
-    )
-    install(
-        FILES ${CMAKE_SOURCE_DIR}/v-gears-installer.png
-        DESTINATION share/pixmaps
-    )
-endif()
-
-# TODO: Generate Installer (.msi, .deb, .appImage...)
+# Install rules for the data, documentation and desktop files, and the packages.
+include(Packaging)

BIN
data/RenderSystem_GL.so.1.12.10


+ 4 - 2
data/plugins.cfg

@@ -1,7 +1,9 @@
 # Defines plugins to load
 # Defines plugins to load
 
 
-# Define plugin folder
-PluginFolder=.
+# Plugin folder. Leave it unset: V-Gears looks for each plugin in the data directory, next to the executable, in the
+# Ogre plugin directory found when it was built, and in the usual system directories. If set (relative to this file),
+# it's searched first.
+# PluginFolder=
 
 
 # Define plugins
 # Define plugins
 # Plugin=RenderSystem_Direct3D9
 # Plugin=RenderSystem_Direct3D9

+ 4 - 2
data/plugins_d.cfg

@@ -1,7 +1,9 @@
 # Defines plugins to load
 # Defines plugins to load
 
 
-# Define plugin folder
-PluginFolder=.
+# Plugin folder. Leave it unset: V-Gears looks for each plugin in the data directory, next to the executable, in the
+# Ogre plugin directory found when it was built, and in the usual system directories. If set (relative to this file),
+# it's searched first.
+# PluginFolder=
 
 
 # Define plugins
 # Define plugins
 # Plugin=RenderSystem_Direct3D9_d
 # Plugin=RenderSystem_Direct3D9_d

+ 0 - 5
debian/changelog

@@ -1,5 +0,0 @@
-v-gears (0.0.1) bullseye; urgency=low
-
-  First VGears Package
-
- -- Iñigo Valentin <i@inigovalentin.com>  Tue, 26 Jul 2022 22:07:00 -0100

+ 0 - 1
debian/compat

@@ -1 +0,0 @@
-7

+ 0 - 15
debian/control

@@ -1,15 +0,0 @@
-Source: v-gears
-Section: games
-Priority: extra
-Maintainer: Iñigo Valentin <i@inigovalentin.com>
-Build-Depends: debhelper(>= 7.0.50~),libogre-1.12-dev, libtinyxml-dev, cmake, cdbs, libois-dev, libboost-program-options-dev, libboost-filesystem-dev, libvorbis-dev, libopenal-dev, libboost-test-dev, zlib1g-dev, libarchive-dev, libqt5core5a, libqt5widgets5, libluajit-5.1-dev
-Standards-Version: 3.9.5
-Homepage: https://v-gears.inigovalentin.com
-Vcs-Git: https://github.com/InigoValentin/V-Gears.git
-#Vcs-Browser: http://git.debian.org/?p=collab-maint/vgears.git;a=summary
-
-Package: v-gears
-Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}
-Description: A Free Final Fantasy VII Engine
- A Free Replacement for the Final Fantasy VII Engine.

+ 0 - 23
debian/copyright

@@ -1,23 +0,0 @@
-Format: http://dep.debian.net/deps/dep5
-Upstream-Name: V-Gears
-Source: http://sourceforge.net/projects/v-gears
-
-Files: debian/*
-Copyright: 2022 The V-Gears team <CCR EMAIL>
-License: GPL-2+
- This package is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
- .
- This package is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- .
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>
- .
- On Debian systems, the complete text of the GNU General
- Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
-

+ 0 - 1
debian/docs

@@ -1 +0,0 @@
-COPYING.txt

+ 0 - 5
debian/install

@@ -1,5 +0,0 @@
-v-gears.desktop /usr/share/applications
-v-gears.png /usr/share/pixmaps
-v-gears-installer.desktop /usr/share/applications
-v-gears-installer.png /usr/share/pixmaps
-data/* /usr/share/v-gears

+ 0 - 3
debian/menu

@@ -1,3 +0,0 @@
-?package(v-gears):needs="x11" section="Games/Toys"\
-  title="v-gears" command="sh -c 'cd ~/.v-gears && exec /usr/games/v-gears'"
-  

+ 0 - 7
debian/prerm

@@ -1,7 +0,0 @@
-#!/bin/bash
-#DEBHELPER#
-set -e
-if [ -d /usr/share/v-gears ];then
-  rm -r /usr/share/v-gears 
-else exit 0
-fi

+ 0 - 3
debian/rules

@@ -1,3 +0,0 @@
-#!/usr/bin/make -f
-include /usr/share/cdbs/1/rules/debhelper.mk
-include /usr/share/cdbs/1/class/cmake.mk

+ 0 - 1
debian/source/format

@@ -1 +0,0 @@
-3.0 (quilt)

+ 73 - 7
doc/BUILD.md

@@ -14,13 +14,10 @@ The CMake scripts include an option `USE_DETERMINISTIC_FALLBACKS` (default `ON`)
 	`find_package`/pkg-config do not succeed (SONAME `find_library` passes,
 	`find_package`/pkg-config do not succeed (SONAME `find_library` passes,
 	appending common `/usr/lib` paths, or injecting generic `-lboost_*` flags).
 	appending common `/usr/lib` paths, or injecting generic `-lboost_*` flags).
 - When `OFF`, CMake will not perform these fallbacks; instead require proper
 - When `OFF`, CMake will not perform these fallbacks; instead require proper
-	dev packages or explicit hints (recommended for CI and packaging).
+	dev packages or explicit hints.
 
 
-Disable fallbacks example:
-
-```bash
-cmake .. -DUSE_DETERMINISTIC_FALLBACKS=OFF -DCMAKE_BUILD_TYPE=Release
-```
+**Leave it `ON` for now.** On Linux, the regular library detection in `src/CMakeLists.txt` is skipped because of a
+known bug in the script, so with the fallbacks `OFF` the build fails to link Boost and OgreBites.
 
 
 If you see warnings about missing Boost or OgreBites, prefer installing the
 If you see warnings about missing Boost or OgreBites, prefer installing the
 appropriate `-dev` packages listed above, or point CMake at your OGRE/Boost
 appropriate `-dev` packages listed above, or point CMake at your OGRE/Boost
@@ -45,7 +42,7 @@ Once the build process finishes, you u sholh have the following files (from the
 - `build/bin/v-gears`, the engine executable.
 - `build/bin/v-gears`, the engine executable.
 - `build/bin/v-gears-installer`, the data installer.
 - `build/bin/v-gears-installer`, the data installer.
 
 
-The engine must be launched from the installation directory (the installer starts it from there for you), so before trying to run it, keep reading.
+The engine can be launched from anywhere: it finds its data directory (`~/.v-gears/` by default) and fills it from the `data/` folder of the repository the first time. See the [run guide](RUN.md "Run V-Gears") for details.
 
 
 ## Run the tests (optional)
 ## Run the tests (optional)
 
 
@@ -59,6 +56,75 @@ make v-gears-installer-tests;
 ./bin/v-gears-installer-tests
 ./bin/v-gears-installer-tests
 ```
 ```
 
 
+## Make packages (optional)
+
+V-Gears can be packaged for Linux as a `.deb` package and as a `.tar.gz` archive. None of them includes data from the original game: the player still needs the data installer and their own copy of the game.
+
+### Build the packages
+
+Use a separate Release build, so it doesn't interfere with your development build. From the project directory:
+
+```bash
+mkdir build-release;
+cd build-release;
+cmake ../ -DCMAKE_BUILD_TYPE=Release;
+make -j4 v-gears v-gears-installer;
+cpack;
+```
+
+The first build takes a while. Adjust `-j` to the number of cores and the memory of your system: each compiler process can use a lot of memory, so don't go as high as the number of cores if memory is short.
+
+When `cpack` finishes, `build-release/` contains:
+
+- `v-gears_<version>_amd64.deb`, the Debian package.
+- `v-gears-<version>-Linux-x86_64.tar.gz`, the archive.
+- A `.sha256` checksum file for each of them. Publish them along with the packages.
+
+The version is the one in the `project()` line of the main `CMakeLists.txt`. To make only one of the packages, use `cpack -G DEB` or `cpack -G TGZ`.
+
+### What's in the packages
+
+Both have the same files:
+
+- `bin/v-gears` and `bin/v-gears-installer`, the engine and the data installer.
+- `share/v-gears/`, the V-Gears data files (configuration, scripts, screens, fonts...). The engine copies them to the user's data directory (`~/.v-gears/`) the first time it runs, and the data installer adds the game data there.
+- `share/doc/v-gears/`, the documentation and the license.
+- The desktop files and the icons, so both programs show in the applications menu.
+
+The `.deb` installs them under `/usr`. Install it with `apt`, so it installs the dependencies too:
+
+```bash
+sudo apt install ./v-gears_0.3.0_amd64.deb
+```
+
+The `.tar.gz` has the same layout in a single folder. It can be extracted anywhere and run from there (`bin/v-gears`, `bin/v-gears-installer`), without root.
+
+The Ogre plugins (`RenderSystem_GL`, `Codec_STBI`) are not in the packages: the engine loads the ones of the system's Ogre.
+
+### Which systems can use them
+
+The packages don't include any library: they use the ones installed in the system. The dependencies of the `.deb` are taken from the libraries the programs were built with, so it can only be installed in systems that have the same versions (the same Ogre and Boost versions, especially). In practice, the packages work in the same distribution and version where they were built.
+
+To make packages for a given distribution, build them in that distribution, for example in a container or a virtual machine. The `.tar.gz` has the same limits, but it gives no warning: if a library is missing, the programs don't start.
+
+### Build options
+
+These can be set when running `cmake` (`-D<option>=<value>`):
+
+|Option|Default|Description|
+|--|--|--|
+|`CMAKE_BUILD_TYPE`|`Release`|Use `Release` for packages. A `Debug` build makes much larger programs, and its archive name ends in `-debug`.|
+|`VGEARS_OGRE_PLUGIN_DIR`|Found at build time|Directory of the system's Ogre plugins. The engine looks for the plugins there after its data directory and its own directory. Set it if the plugins of the target system are in another place.|
+|`VGEARS_PACKAGE_MAINTAINER`|The project maintainer|Name and email of the maintainer in the `.deb`.|
+
+### Install without a package
+
+To install V-Gears directly in the system, with the same files and layout as the packages:
+
+```bash
+sudo cmake --install . --prefix /usr/local
+```
+
 ## Optional programs
 ## Optional programs
 
 
 To convert the sound effects and the music, the installer uses FFmpeg and TiMidity. They are not needed to build V-Gears, only to install the data. See the [data installation guide](INSTALL_DATA.md "Install Data").
 To convert the sound effects and the music, the installer uses FFmpeg and TiMidity. They are not needed to build V-Gears, only to install the data. See the [data installation guide](INSTALL_DATA.md "Install Data").

+ 3 - 2
doc/INSTALL.md

@@ -12,11 +12,12 @@ The data installer can do this for you: enable "Copy V-Gears data files" in its
   ├── field_models_and_animation_metadata.xml
   ├── field_models_and_animation_metadata.xml
   ├── plugins.cfg
   ├── plugins.cfg
   ├── plugins_d.cfg
   ├── plugins_d.cfg
-  ├── RenderSystem_GL.so.1.12.10
   ├── resources.cfg
   ├── resources.cfg
   └── resources_d.cfg
   └── resources_d.cfg
 ```
 ```
-3. Whenever you want to launch V-Gears or the V-Gears data installer, it must be done from this directory.
+3. If you used a directory other than `~/.v-gears/`, start V-Gears from it, or pass it with `--data-dir` (see [Run V-Gears](RUN.md "Run V-Gears")).
+
+If you skip these steps, V-Gears copies the files to `~/.v-gears/` itself the first time it runs.
 
 
 ## Next steps
 ## Next steps
 
 

+ 12 - 5
doc/RUN.md

@@ -4,15 +4,22 @@
 
 
 If you have followed the [build](BUILD.md "Building guide") and [installaton](INSTALL.md "Installation guide") guides, you'll have the game data installed to `~/.v-gears/`, and the binaries compilled somewhere (we'll assum they are in `~/V-Gears/build/bin`, change the directories according to your system).
 If you have followed the [build](BUILD.md "Building guide") and [installaton](INSTALL.md "Installation guide") guides, you'll have the game data installed to `~/.v-gears/`, and the binaries compilled somewhere (we'll assum they are in `~/V-Gears/build/bin`, change the directories according to your system).
 
 
-On a terminal, do into the game data folder, and from there, run the engine
+Run the engine from any directory:
 
 
 ```Bash
 ```Bash
-cd ~/.v-gears/;
-~/V-Gears/build/bin/v-gears;
-
+~/V-Gears/build/bin/v-gears
 ```
 ```
 
 
-The data installer also has a **Launch game** button, which starts the engine from the installation directory.
+The engine looks for its data directory in this order, and changes into it:
+
+1. The directory given with `--data-dir`, for example `v-gears --data-dir ~/games/v-gears`.
+2. The directory in the `VGEARS_DATA_DIR` environment variable.
+3. The current directory, if it has the V-Gears files (`plugins.cfg` and a `data` folder).
+4. `~/.v-gears/` (`%USERPROFILE%\v-gears\` in Windows), the same default as the data installer.
+
+If the chosen directory doesn't have the V-Gears files yet, the engine copies them there from the `data/` folder of the repository (or from `share/v-gears` in an installed package; `VGEARS_TEMPLATE_DIR` overrides it). Files that are already there are never overwritten. The data of the original game still has to be added with the data installer.
+
+The data installer also has a **Launch game** button, which starts the engine with the installation directory.
 
 
 ## Key Mappings
 ## Key Mappings
 
 

+ 34 - 9
src/CMakeLists.txt

@@ -593,6 +593,8 @@ set(VGEARS_SOURCE_FILES
     common/FinalFantasy7/FF7NameLookup.cpp
     common/FinalFantasy7/FF7NameLookup.cpp
     common/LzsFile.cpp
     common/LzsFile.cpp
     common/TypeDefine.cpp
     common/TypeDefine.cpp
+    common/DataDirectory.cpp
+    common/OgrePlugins.cpp
     common/VGearsApplication.cpp
     common/VGearsApplication.cpp
     common/VGearsManualObject.cpp
     common/VGearsManualObject.cpp
     common/VGearsResource.cpp
     common/VGearsResource.cpp
@@ -839,6 +841,18 @@ if(_test_removed_count GREATER 0)
     message(STATUS "Removed ${_test_removed_count} decompiler test source(s) from libvgears")
     message(STATUS "Removed ${_test_removed_count} decompiler test source(s) from libvgears")
 endif()
 endif()
 
 
+# Directory with the system's Ogre plugins (RenderSystem_GL, Codec_STBI...). V-Gears looks for the plugins listed in
+# plugins.cfg there after the data directory and the directory of the executable (see common/OgrePlugins.h).
+# Packagers can set it, for example -DVGEARS_OGRE_PLUGIN_DIR=/usr/lib/x86_64-linux-gnu/OGRE.
+set(_vgears_default_plugin_dir "")
+if(OGRE_PLUGIN_DIR)
+    set(_vgears_default_plugin_dir "${OGRE_PLUGIN_DIR}")
+elseif(PKG_CONFIG_FOUND)
+    pkg_get_variable(_vgears_default_plugin_dir OGRE plugindir)
+endif()
+set(VGEARS_OGRE_PLUGIN_DIR "${_vgears_default_plugin_dir}" CACHE PATH "Directory of the Ogre plugins")
+message(STATUS "Ogre plugin directory: ${VGEARS_OGRE_PLUGIN_DIR}")
+
 add_library(libvgears STATIC ${VGEARS_SOURCE_FILES})
 add_library(libvgears STATIC ${VGEARS_SOURCE_FILES})
 target_sources(libvgears PRIVATE installer/SfxDump.c)
 target_sources(libvgears PRIVATE installer/SfxDump.c)
 # Ensure generated ui headers are created before compiling libvgears
 # Ensure generated ui headers are created before compiling libvgears
@@ -877,6 +891,7 @@ if(DEFINED _GENERATED_UI_TARGET)
 endif()
 endif()
     target_link_libraries(libvgears PUBLIC ${VGEARS_LUAJIT_LIBRARIES})
     target_link_libraries(libvgears PUBLIC ${VGEARS_LUAJIT_LIBRARIES})
 target_compile_definitions(libvgears PUBLIC TIXML_USE_STL)
 target_compile_definitions(libvgears PUBLIC TIXML_USE_STL)
+target_compile_definitions(libvgears PUBLIC VGEARS_OGRE_PLUGIN_DIR="${VGEARS_OGRE_PLUGIN_DIR}")
 if(NOT MSVC)
 if(NOT MSVC)
     target_compile_options(libvgears PRIVATE -fPIC)
     target_compile_options(libvgears PRIVATE -fPIC)
 endif()
 endif()
@@ -923,8 +938,6 @@ endforeach()
 
 
 add_executable(v-gears ${VGEARS_SOURCE_FILES})
 add_executable(v-gears ${VGEARS_SOURCE_FILES})
 SET_PROPERTY(TARGET v-gears PROPERTY FOLDER "build/v-gears")
 SET_PROPERTY(TARGET v-gears PROPERTY FOLDER "build/v-gears")
-set(CPACK_PACKAGE_EXECUTABLES v-gears "v-gears")
-set(CPACK_WIX_PROGRAM_MENU_FOLDER "VGears")
 if(APPLE)
 if(APPLE)
     target_link_libraries(v-gears "-framework CoreFoundation -framework Cocoa -framework IOKit" ${ois_lib})
     target_link_libraries(v-gears "-framework CoreFoundation -framework Cocoa -framework IOKit" ${ois_lib})
 endif()
 endif()
@@ -942,6 +955,22 @@ set(VGEARS_LIBRARIES
     ${OGRE_LIBRARIES}
     ${OGRE_LIBRARIES}
 )
 )
 
 
+# Boost 1.70 and newer ship CMake config files that create the Boost::<component> targets. Use them if the search
+# above found nothing. Note that, outside MinGW, that search doesn't run: the if(MINGW) block near the top of this file
+# is never closed where it should be, and it covers all the library detection down to the source list.
+# Boost.System and Boost.Chrono are optional: System is header-only in recent versions.
+if(NOT BOOST_LINK_LIBS)
+    find_package(Boost CONFIG QUIET COMPONENTS program_options filesystem thread OPTIONAL_COMPONENTS system chrono)
+    foreach(_comp IN ITEMS program_options filesystem thread system chrono)
+        if(TARGET Boost::${_comp})
+            list(APPEND BOOST_LINK_LIBS Boost::${_comp})
+        endif()
+    endforeach()
+    if(BOOST_LINK_LIBS)
+        message(STATUS "Found Boost ${Boost_VERSION} (CMake config): ${BOOST_LINK_LIBS}")
+    endif()
+endif()
+
 # Ensure we have something for Boost; if BOOST_LINK_LIBS is empty, inject
 # Ensure we have something for Boost; if BOOST_LINK_LIBS is empty, inject
 # generic `-lboost_*` flags directly so the system linker will attempt to
 # generic `-lboost_*` flags directly so the system linker will attempt to
 # resolve them from standard library search paths.
 # resolve them from standard library search paths.
@@ -1035,18 +1064,14 @@ if(UNIX)
     target_compile_definitions(v-gears PRIVATE POSIX)
     target_compile_definitions(v-gears PRIVATE POSIX)
 endif()
 endif()
 target_compile_definitions(v-gears PRIVATE TIXML_USE_STL)
 target_compile_definitions(v-gears PRIVATE TIXML_USE_STL)
+target_compile_definitions(v-gears PRIVATE VGEARS_OGRE_PLUGIN_DIR="${VGEARS_OGRE_PLUGIN_DIR}")
 if(NOT MSVC)
 if(NOT MSVC)
     target_compile_options(v-gears PRIVATE -fPIC)
     target_compile_options(v-gears PRIVATE -fPIC)
 endif()
 endif()
 
 
 
 
-# Install v-gears.
-# TODO: Fix routes for installation
-if(WIN32 OR APPLE)
-    install(TARGETS v-gears DESTINATION .)
-else()
-    install(TARGETS v-gears RUNTIME DESTINATION ./games)
-endif()
+# Install v-gears (see CMake/InstallDirs.cmake).
+install(TARGETS v-gears RUNTIME DESTINATION ${VGEARS_INSTALL_BINDIR} BUNDLE DESTINATION ${VGEARS_INSTALL_BINDIR})
 
 
 
 
 # If installer build is enabled, ensure Qt includes/links are available for the
 # If installer build is enabled, ensure Qt includes/links are available for the

+ 200 - 0
src/common/DataDirectory.cpp

@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include "common/DataDirectory.h"
+#include <cstdlib>
+#ifdef _WIN32
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+#include <windows.h>
+#else
+#include <pwd.h>
+#include <unistd.h>
+#endif
+
+namespace VGears{
+
+    namespace DataDirectory{
+
+        namespace bfs = boost::filesystem;
+
+        const char* ENV_DATA_DIR("VGEARS_DATA_DIR");
+
+        const char* ENV_TEMPLATE_DIR("VGEARS_TEMPLATE_DIR");
+
+        namespace{
+
+            /**
+             * Checks if two paths are the same directory, without failing if one of them doesn't exist.
+             */
+            bool SameDirectory(const bfs::path& a, const bfs::path& b){
+                if (a.empty() || b.empty()) return false;
+                boost::system::error_code ec;
+                const bool same = bfs::equivalent(a, b, ec);
+                if (!ec) return same;
+                return a.lexically_normal() == b.lexically_normal();
+            }
+
+            /**
+             * Checks if a directory exists and has something inside.
+             */
+            bool IsNonEmptyDirectory(const bfs::path& directory){
+                boost::system::error_code ec;
+                if (!bfs::is_directory(directory, ec)) return false;
+                return bfs::directory_iterator(directory, ec) != bfs::directory_iterator();
+            }
+
+            /**
+             * Gets the home directory of the user, or an empty path.
+             */
+            bfs::path HomeDirectory(){
+#ifdef _WIN32
+                if (const char* profile = std::getenv("USERPROFILE")) if (*profile) return bfs::path(profile);
+                const char* drive = std::getenv("HOMEDRIVE");
+                const char* path = std::getenv("HOMEPATH");
+                if (drive && path) return bfs::path(std::string(drive) + path);
+                return bfs::path();
+#else
+                if (const char* home = std::getenv("HOME")) if (*home) return bfs::path(home);
+                if (const passwd* user = getpwuid(getuid())) if (user->pw_dir) return bfs::path(user->pw_dir);
+                return bfs::path();
+#endif
+            }
+        }
+
+        bool IsDataDirectory(const bfs::path& directory){
+            boost::system::error_code ec;
+            return bfs::is_regular_file(directory / "plugins.cfg", ec) && bfs::is_directory(directory / "data", ec);
+        }
+
+        bfs::path DefaultDirectory(){
+            const bfs::path home = HomeDirectory();
+            if (home.empty()) return bfs::path();
+#ifdef _WIN32
+            return home / "v-gears";
+#else
+            return home / ".v-gears";
+#endif
+        }
+
+        bfs::path ExecutableDirectory(const char* argv0){
+            boost::system::error_code ec;
+#ifdef _WIN32
+            wchar_t buffer[MAX_PATH * 4];
+            const DWORD length = GetModuleFileNameW(nullptr, buffer, sizeof(buffer) / sizeof(buffer[0]));
+            if (length > 0 && length < sizeof(buffer) / sizeof(buffer[0]))
+                return bfs::path(std::wstring(buffer, length)).parent_path();
+#else
+            const bfs::path self = bfs::read_symlink("/proc/self/exe", ec);
+            if (!ec && !self.empty()) return self.parent_path();
+#endif
+            if (argv0 == nullptr || *argv0 == '\0') return bfs::current_path(ec);
+            return bfs::absolute(bfs::path(argv0)).lexically_normal().parent_path();
+        }
+
+        std::vector<bfs::path> TemplateCandidates(const bfs::path& executable_directory){
+            std::vector<bfs::path> candidates;
+            if (const char* env = std::getenv(ENV_TEMPLATE_DIR)) if (*env) candidates.emplace_back(env);
+            const bfs::path& exe = executable_directory;
+            // Portable folder: <dir>/v-gears(.exe) and <dir>/data/plugins.cfg.
+            candidates.push_back(exe / "data");
+            // Installed package: <prefix>/bin or <prefix>/games, and <prefix>/share/v-gears.
+            candidates.push_back((exe / ".." / "share" / "v-gears").lexically_normal());
+            // Build tree: <repository>/build/bin and <repository>/data. Also <repository>/build/<type>/bin.
+            candidates.push_back((exe / ".." / "data").lexically_normal());
+            candidates.push_back((exe / ".." / ".." / "data").lexically_normal());
+            candidates.push_back((exe / ".." / ".." / ".." / "data").lexically_normal());
+#ifndef _WIN32
+            candidates.push_back("/usr/share/v-gears");
+            candidates.push_back("/usr/local/share/v-gears");
+#endif
+            return candidates;
+        }
+
+        bfs::path FindTemplate(const std::vector<bfs::path>& candidates, const bfs::path& exclude){
+            for (const bfs::path& candidate : candidates){
+                if (SameDirectory(candidate, exclude)) continue;
+                if (IsDataDirectory(candidate)) return candidate;
+            }
+            return bfs::path();
+        }
+
+        unsigned int CopyMissing(const bfs::path& from, const bfs::path& to){
+            unsigned int copied = 0;
+            bfs::create_directories(to);
+            for (bfs::recursive_directory_iterator it(from), end; it != end; ++ it){
+                const bfs::path relative = it->path().lexically_relative(from);
+                const bfs::path target = to / relative;
+                if (bfs::is_directory(it->status())) bfs::create_directories(target);
+                else if (bfs::is_regular_file(it->status()) && !bfs::exists(target)){
+                    bfs::create_directories(target.parent_path());
+                    bfs::copy_file(it->path(), target);
+                    ++ copied;
+                }
+            }
+            return copied;
+        }
+
+        Resolution Resolve(
+          const std::string& requested, const bfs::path& current, const bfs::path& default_directory,
+          const std::vector<bfs::path>& template_candidates
+        ){
+            Resolution result;
+            bfs::path directory;
+            if (!requested.empty()) directory = bfs::absolute(bfs::path(requested), current);
+            else if (IsDataDirectory(current)) directory = current;
+            else directory = default_directory;
+            if (directory.empty()){
+                result.error = "The home directory can't be found. Use --data-dir or set "
+                  + std::string(ENV_DATA_DIR) + " to choose the data directory.";
+                return result;
+            }
+            directory = directory.lexically_normal();
+
+            if (!IsDataDirectory(directory)){
+                if (IsNonEmptyDirectory(directory) && !bfs::is_directory(directory / "data")){
+                    result.error = "The directory " + directory.string() + " is not empty and has no V-Gears files, "
+                      "so they are not copied there. Choose an empty or V-Gears directory.";
+                    return result;
+                }
+                const bfs::path source = FindTemplate(template_candidates, directory);
+                if (source.empty()){
+                    result.error = "The directory " + directory.string() + " has no V-Gears files, and the files "
+                      "shipped with V-Gears were not found to copy them. Reinstall V-Gears, or set "
+                      + std::string(ENV_TEMPLATE_DIR) + " to the folder that contains plugins.cfg and data/.";
+                    return result;
+                }
+                try{
+                    result.copied_files = CopyMissing(source, directory);
+                }
+                catch (const bfs::filesystem_error& e){
+                    result.error = "The V-Gears files could not be copied from " + source.string() + " to "
+                      + directory.string() + ": " + e.what();
+                    return result;
+                }
+                result.seeded = true;
+                result.template_directory = source;
+                if (!IsDataDirectory(directory)){
+                    result.error = "The V-Gears files were copied to " + directory.string()
+                      + ", but it's still missing plugins.cfg or the data folder.";
+                    return result;
+                }
+            }
+            result.directory = directory;
+            return result;
+        }
+    }
+}

+ 167 - 0
src/common/DataDirectory.h

@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#pragma once
+
+#include <string>
+#include <vector>
+#include <boost/filesystem.hpp>
+
+namespace VGears{
+
+    /**
+     * Locates the directory the game runs from.
+     *
+     * The data directory holds the configuration (plugins.cfg, resources.cfg...), the game data in "data/", and
+     * everything the game writes (saves, logs, temporary files). The game reads all of it relative to the working
+     * directory, so it changes into the data directory on start.
+     *
+     * A read-only template of the data directory (the "data" folder of the repository, or "share/v-gears" in an
+     * installed package) is shipped with the program. When the data directory has no V-Gears files yet, the files of
+     * the template are copied there. The game data extracted from the original game is added later by the data
+     * installer.
+     */
+    namespace DataDirectory{
+
+        /**
+         * Name of the environment variable that selects the data directory.
+         */
+        extern const char* ENV_DATA_DIR;
+
+        /**
+         * Name of the environment variable that selects the template directory.
+         */
+        extern const char* ENV_TEMPLATE_DIR;
+
+        /**
+         * Result of {@see Resolve}.
+         */
+        struct Resolution{
+
+            /**
+             * Absolute path of the data directory. Empty on error.
+             */
+            boost::filesystem::path directory;
+
+            /**
+             * True if files were copied from a template.
+             */
+            bool seeded = false;
+
+            /**
+             * The template the files were copied from, if any.
+             */
+            boost::filesystem::path template_directory;
+
+            /**
+             * Number of files copied from the template.
+             */
+            unsigned int copied_files = 0;
+
+            /**
+             * Description of the error. Empty on success.
+             */
+            std::string error;
+        };
+
+        /**
+         * Checks if a directory has the V-Gears files: a plugins.cfg file and a "data" subdirectory.
+         *
+         * @param[in] directory The directory to check.
+         * @return True if the directory has the V-Gears files.
+         */
+        bool IsDataDirectory(const boost::filesystem::path& directory);
+
+        /**
+         * Gets the default data directory of the user.
+         *
+         * It is "~/.v-gears" in Linux and macOS, and "%USERPROFILE%\v-gears" in Windows, the same defaults the data
+         * installer uses.
+         *
+         * @return The default data directory, or an empty path if the home directory can't be found.
+         */
+        boost::filesystem::path DefaultDirectory();
+
+        /**
+         * Gets the directory of the running executable.
+         *
+         * @param[in] argv0 The first command line argument, used if the system can't tell the executable path.
+         * @return The absolute directory of the executable.
+         */
+        boost::filesystem::path ExecutableDirectory(const char* argv0);
+
+        /**
+         * Gets the places where the template can be, in order of preference.
+         *
+         * They cover a build tree (<repository>/build/bin next to <repository>/data), an installed package
+         * (<prefix>/bin or <prefix>/games next to <prefix>/share/v-gears) and a portable folder (a "data" folder
+         * next to the executable).
+         *
+         * @param[in] executable_directory The directory of the running executable.
+         * @return The candidate directories. They may not exist.
+         */
+        std::vector<boost::filesystem::path> TemplateCandidates(
+          const boost::filesystem::path& executable_directory
+        );
+
+        /**
+         * Finds the first candidate that has the V-Gears files.
+         *
+         * @param[in] candidates The candidates to check, in order of preference.
+         * @param[in] exclude A directory that must not be returned, even if it qualifies.
+         * @return The template, or an empty path if no candidate qualifies.
+         */
+        boost::filesystem::path FindTemplate(
+          const std::vector<boost::filesystem::path>& candidates, const boost::filesystem::path& exclude
+        );
+
+        /**
+         * Copies the files of a directory tree that are missing in another one.
+         *
+         * Existing files are never overwritten, so the changes of the user and the data extracted by the installer
+         * are kept.
+         *
+         * @param[in] from The source directory.
+         * @param[in] to The destination directory. It's created if it doesn't exist.
+         * @return The number of files copied.
+         * @throws boost::filesystem::filesystem_error If a file or directory can't be read or written.
+         */
+        unsigned int CopyMissing(const boost::filesystem::path& from, const boost::filesystem::path& to);
+
+        /**
+         * Decides the data directory and fills it from a template if needed.
+         *
+         * The data directory is, in order:
+         * - The requested directory, if any (from the command line or the environment).
+         * - The current directory, if it has the V-Gears files.
+         * - The default directory.
+         *
+         * If the chosen directory doesn't have the V-Gears files, the missing files are copied from the first
+         * template candidate that has them. A directory that is not empty and has no "data" subdirectory is not
+         * filled, to avoid writing the files into an unrelated directory chosen by mistake.
+         *
+         * @param[in] requested The requested directory. Empty if none. If relative, it's relative to current.
+         * @param[in] current The current directory.
+         * @param[in] default_directory The default data directory.
+         * @param[in] template_candidates The template candidates, in order of preference.
+         * @return The resolution. Check its error field.
+         */
+        Resolution Resolve(
+          const std::string& requested, const boost::filesystem::path& current,
+          const boost::filesystem::path& default_directory,
+          const std::vector<boost::filesystem::path>& template_candidates
+        );
+    }
+}

+ 113 - 0
src/common/OgrePlugins.cpp

@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include "common/OgrePlugins.h"
+#include <algorithm>
+#include <fstream>
+
+namespace VGears{
+
+    namespace OgrePlugins{
+
+        namespace bfs = boost::filesystem;
+
+        namespace{
+
+            std::string Trim(const std::string& text){
+                const char* blanks = " \t\r\n";
+                const size_t first = text.find_first_not_of(blanks);
+                if (first == std::string::npos) return "";
+                return text.substr(first, text.find_last_not_of(blanks) - first + 1);
+            }
+
+            /**
+             * Usual directories of the Ogre plugins installed by the system.
+             */
+            const std::vector<bfs::path> SYSTEM_DIRECTORIES = {
+#ifndef _WIN32
+              "/usr/lib/x86_64-linux-gnu/OGRE", "/usr/lib/aarch64-linux-gnu/OGRE", "/usr/lib/i386-linux-gnu/OGRE",
+              "/usr/lib64/OGRE", "/usr/lib/OGRE", "/usr/local/lib/OGRE", "/usr/local/lib64/OGRE"
+#endif
+            };
+        }
+
+        bool ReadConfig(const bfs::path& file, Config& config){
+            config = Config();
+            std::ifstream in(file.string());
+            if (!in.good()) return false;
+            std::string line;
+            while (std::getline(in, line)){
+                line = Trim(line);
+                if (line.empty() || line[0] == '#') continue;
+                const size_t equals = line.find('=');
+                if (equals == std::string::npos) continue;
+                const std::string key = Trim(line.substr(0, equals));
+                const std::string value = Trim(line.substr(equals + 1));
+                if (key == "PluginFolder") config.folder = value;
+                else if (key == "Plugin" && !value.empty()) config.plugins.push_back(value);
+            }
+            return true;
+        }
+
+        std::vector<bfs::path> SearchDirectories(
+          const bfs::path& config_directory, const std::string& config_folder,
+          const bfs::path& executable_directory, const std::string& build_directory
+        ){
+            std::vector<bfs::path> directories;
+            auto add = [&directories](const bfs::path& directory){
+                if (directory.empty()) return;
+                const bfs::path normal = directory.lexically_normal();
+                if (std::find(directories.begin(), directories.end(), normal) == directories.end())
+                    directories.push_back(normal);
+            };
+            if (!config_folder.empty()) add(bfs::absolute(bfs::path(config_folder), config_directory));
+            add(config_directory);
+            if (!executable_directory.empty()){
+                add(executable_directory);
+                add(executable_directory / ".." / "lib" / "OGRE");
+                add(executable_directory / ".." / "lib");
+            }
+            if (!build_directory.empty()) add(bfs::path(build_directory));
+            for (const bfs::path& directory : SYSTEM_DIRECTORIES) add(directory);
+            return directories;
+        }
+
+        std::vector<std::string> FileNames(const std::string& name, const std::string& ogre_version){
+#if defined(_WIN32)
+            (void)ogre_version;
+            return {name + ".dll"};
+#elif defined(__APPLE__)
+            (void)ogre_version;
+            return {name + ".dylib", name + ".framework"};
+#else
+            return {name + ".so." + ogre_version};
+#endif
+        }
+
+        bfs::path Find(const std::string& name, const std::vector<bfs::path>& directories, const std::string& ogre_version){
+            boost::system::error_code ec;
+            const bfs::path as_path(name);
+            if (as_path.is_absolute()) return bfs::exists(as_path, ec) ? as_path : bfs::path();
+            const std::vector<std::string> names = FileNames(name, ogre_version);
+            for (const bfs::path& directory : directories){
+                for (const std::string& file_name : names){
+                    const bfs::path file = directory / file_name;
+                    if (bfs::exists(file, ec)) return file;
+                }
+            }
+            return bfs::path();
+        }
+    }
+}

+ 106 - 0
src/common/OgrePlugins.h

@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#pragma once
+
+#include <string>
+#include <vector>
+#include <boost/filesystem.hpp>
+
+namespace VGears{
+
+    /**
+     * Locates the Ogre plugins listed in plugins.cfg.
+     *
+     * Ogre only looks for the plugins in the single PluginFolder of plugins.cfg, so the same file can't work in a
+     * build tree, an installed package and a portable folder. V-Gears loads the plugins itself instead, looking for
+     * each one in several directories (see {@see SearchDirectories}).
+     *
+     * It doesn't depend on Ogre, so the installer and the tests can use it.
+     */
+    namespace OgrePlugins{
+
+        /**
+         * Contents of a plugins.cfg file.
+         */
+        struct Config{
+
+            /**
+             * Value of PluginFolder. Empty if it's not set.
+             */
+            std::string folder;
+
+            /**
+             * Names of the plugins, in order (the values of the Plugin lines).
+             */
+            std::vector<std::string> plugins;
+        };
+
+        /**
+         * Reads a plugins.cfg file.
+         *
+         * @param[in] file Path to the file.
+         * @param[out] config The contents of the file.
+         * @return False if the file can't be read.
+         */
+        bool ReadConfig(const boost::filesystem::path& file, Config& config);
+
+        /**
+         * Gets the directories where the plugins are looked for, in order of preference.
+         *
+         * They are:
+         * - The PluginFolder of plugins.cfg, if set (relative to the directory of plugins.cfg).
+         * - The directory of plugins.cfg (the data directory).
+         * - The directory of the executable, and "../lib/OGRE" and "../lib" from it (bundles and portable folders).
+         * - The Ogre plugin directory found when V-Gears was built.
+         * - The usual system directories of the Ogre plugins.
+         *
+         * @param[in] config_directory The directory of plugins.cfg.
+         * @param[in] config_folder The PluginFolder of plugins.cfg. Can be empty.
+         * @param[in] executable_directory The directory of the executable. Can be empty.
+         * @param[in] build_directory The Ogre plugin directory found when V-Gears was built. Can be empty.
+         * @return The directories, without duplicates. They may not exist.
+         */
+        std::vector<boost::filesystem::path> SearchDirectories(
+          const boost::filesystem::path& config_directory, const std::string& config_folder,
+          const boost::filesystem::path& executable_directory, const std::string& build_directory
+        );
+
+        /**
+         * Gets the file names a plugin can have in this system.
+         *
+         * In Linux, Ogre loads "<name>.so.<version>". In Windows, "<name>.dll". In macOS, "<name>.dylib" or
+         * "<name>.framework".
+         *
+         * @param[in] name Name of the plugin, as in plugins.cfg.
+         * @param[in] ogre_version The Ogre version, as "major.minor.patch".
+         * @return The file names, in order of preference.
+         */
+        std::vector<std::string> FileNames(const std::string& name, const std::string& ogre_version);
+
+        /**
+         * Finds a plugin.
+         *
+         * @param[in] name Name of the plugin, as in plugins.cfg. If it's an absolute path, it's used as is.
+         * @param[in] directories The directories to look in, in order of preference.
+         * @param[in] ogre_version The Ogre version, as "major.minor.patch".
+         * @return The path of the plugin file, or an empty path if it's not found.
+         */
+        boost::filesystem::path Find(
+          const std::string& name, const std::vector<boost::filesystem::path>& directories,
+          const std::string& ogre_version
+        );
+    }
+}

+ 96 - 7
src/common/VGearsApplication.cpp

@@ -13,6 +13,7 @@
  * GNU General Public License for more details.
  * GNU General Public License for more details.
  */
  */
 
 
+#include <cstdlib>
 #include <iostream>
 #include <iostream>
 #include <boost/filesystem.hpp>
 #include <boost/filesystem.hpp>
 #include <boost/program_options.hpp>
 #include <boost/program_options.hpp>
@@ -21,6 +22,8 @@
 #include <OgreLogManager.h>
 #include <OgreLogManager.h>
 #include <OgreConfigDialog.h>
 #include <OgreConfigDialog.h>
 #include "common/VGearsApplication.h"
 #include "common/VGearsApplication.h"
+#include "common/DataDirectory.h"
+#include "common/OgrePlugins.h"
 #include "VGearsGameState.h"
 #include "VGearsGameState.h"
 #include "data/VGearsAFileManager.h"
 #include "data/VGearsAFileManager.h"
 #include "data/VGearsBackgroundFileManager.h"
 #include "data/VGearsBackgroundFileManager.h"
@@ -44,6 +47,13 @@
  */
  */
 template<> VGears::Application* Ogre::Singleton<VGears::Application>::msSingleton = nullptr;
 template<> VGears::Application* Ogre::Singleton<VGears::Application>::msSingleton = nullptr;
 
 
+/**
+ * Directory with the Ogre plugins, found when V-Gears was built. Set by CMake (VGEARS_OGRE_PLUGIN_DIR).
+ */
+#ifndef VGEARS_OGRE_PLUGIN_DIR
+#define VGEARS_OGRE_PLUGIN_DIR ""
+#endif
+
 namespace VGears{
 namespace VGears{
 
 
     namespace bfs = boost::filesystem;
     namespace bfs = boost::filesystem;
@@ -67,6 +77,11 @@ namespace VGears{
     const char* Application::CLI_RESOURCES_FILE_DESCRIPTION(
     const char* Application::CLI_RESOURCES_FILE_DESCRIPTION(
       "path to config file containing available resources"
       "path to config file containing available resources"
     );
     );
+    const char* Application::CLI_DATA_DIR("data-dir");
+    const char* Application::CLI_DATA_DIR_DESCRIPTION(
+      "Directory with the game data and configuration, where saves and logs are written. "
+      "Defaults to $VGEARS_DATA_DIR, the current directory if it has the V-Gears files, or ~/.v-gears"
+    );
 
 
     Application::Application(int argc, char *argv[]): _argc(argc), _argv(argv){}
     Application::Application(int argc, char *argv[]): _argc(argc), _argv(argv){}
 
 
@@ -81,9 +96,9 @@ namespace VGears{
             if (!processCommandLine(_argc, _argv)) return false;
             if (!processCommandLine(_argc, _argv)) return false;
         }
         }
 
 
-        _root = std::make_unique<Ogre::Root>(
-          plugins_filename_, config_filename_, log_filename_
-        );
+        // No plugins file is passed to Ogre: the plugins are looked for in several places, see loadPlugins.
+        _root = std::make_unique<Ogre::Root>("", config_filename_, log_filename_);
+        loadPlugins();
         _overlay_system = std::make_unique<Ogre::OverlaySystem>();
         _overlay_system = std::make_unique<Ogre::OverlaySystem>();
 
 
         Ogre::Log* default_log(
         Ogre::Log* default_log(
@@ -119,6 +134,43 @@ namespace VGears{
         return true;
         return true;
     }
     }
 
 
+    void Application::loadPlugins(){
+        const bfs::path plugins_file(bfs::absolute(bfs::path(plugins_filename_)));
+        OgrePlugins::Config config;
+        if (!OgrePlugins::ReadConfig(plugins_file, config))
+            throw std::runtime_error("The Ogre plugins file can't be read: " + plugins_file.string());
+        const std::vector<bfs::path> directories(OgrePlugins::SearchDirectories(
+          plugins_file.parent_path(), config.folder,
+          DataDirectory::ExecutableDirectory(_argc > 0 && _argv ? _argv[0] : nullptr), VGEARS_OGRE_PLUGIN_DIR
+        ));
+        const String ogre_version(
+          std::to_string(OGRE_VERSION_MAJOR) + "." + std::to_string(OGRE_VERSION_MINOR) + "."
+          + std::to_string(OGRE_VERSION_PATCH)
+        );
+        for (const String& name : config.plugins){
+            const bfs::path file(OgrePlugins::Find(name, directories, ogre_version));
+            if (file.empty()){
+                String searched;
+                for (const bfs::path& directory : directories) searched += "\n  " + directory.string();
+                throw std::runtime_error(
+                  "The Ogre plugin " + name + " (listed in " + plugins_file.string() + ") was not found in:"
+                  + searched
+                );
+            }
+            Ogre::LogManager::getSingleton().logMessage("Loading Ogre plugin " + file.string());
+            try{
+                _root->loadPlugin(file.string());
+            }
+            catch (const Ogre::Exception& e){
+                // The exception can point to strings of the plugin library, which is unloaded when the root is
+                // destroyed, before the exception is caught in main. Copy the message now.
+                throw std::runtime_error(
+                  "The Ogre plugin " + file.string() + " could not be loaded: " + e.getFullDescription()
+                );
+            }
+        }
+    }
+
     void Application::loadResourcesConfig(){
     void Application::loadResourcesConfig(){
 
 
         // Load resource paths from config file
         // Load resource paths from config file
@@ -186,6 +238,10 @@ namespace VGears{
             (  CLI_RESOURCES_FILE
             (  CLI_RESOURCES_FILE
               , bpo::value< String >()->default_value(resources_filename)
               , bpo::value< String >()->default_value(resources_filename)
               , CLI_RESOURCES_FILE_DESCRIPTION
               , CLI_RESOURCES_FILE_DESCRIPTION
+           )
+            (  CLI_DATA_DIR
+              , bpo::value< String >()
+              , CLI_DATA_DIR_DESCRIPTION
            )
            )
             ;
             ;
         bpo::variables_map vm;
         bpo::variables_map vm;
@@ -196,11 +252,44 @@ namespace VGears{
         }
         }
 
 
         bpo::notify(vm);
         bpo::notify(vm);
-        config_filename_ = vm[CLI_CONFIG_FILE].as<String>();
-        log_filename_ = vm[CLI_LOG_FILE].as<String>();
-        plugins_filename_ = vm[CLI_PLUGINS_FILE].as<String>();
-        resources_filename_ = vm[CLI_RESOURCES_FILE].as<String>();
 
 
+        // The game changes into the data directory, so the files given in the command line are made absolute first.
+        // The default ones are left relative, to be found in the data directory.
+        const bfs::path start_directory(bfs::current_path());
+        auto file_option = [&](const char* name) -> String{
+            const String value(vm[name].as<String>());
+            if (vm[name].defaulted() || value.empty()) return value;
+            return bfs::absolute(bfs::path(value), start_directory).lexically_normal().string();
+        };
+        config_filename_ = file_option(CLI_CONFIG_FILE);
+        log_filename_ = file_option(CLI_LOG_FILE);
+        plugins_filename_ = file_option(CLI_PLUGINS_FILE);
+        resources_filename_ = file_option(CLI_RESOURCES_FILE);
+
+        String requested_directory;
+        if (vm.count(CLI_DATA_DIR)) requested_directory = vm[CLI_DATA_DIR].as<String>();
+        else if (const char* env = std::getenv(DataDirectory::ENV_DATA_DIR)) requested_directory = env;
+        const DataDirectory::Resolution data_directory(DataDirectory::Resolve(
+          requested_directory, start_directory, DataDirectory::DefaultDirectory(),
+          DataDirectory::TemplateCandidates(DataDirectory::ExecutableDirectory(argv[0]))
+        ));
+        if (!data_directory.error.empty()){
+            std::cerr << "V-Gears can't start: " << data_directory.error << std::endl;
+            return false;
+        }
+        if (data_directory.seeded){
+            std::cout << "Copied " << data_directory.copied_files << " V-Gears files from "
+              << data_directory.template_directory.string() << " to " << data_directory.directory.string()
+              << ". Run v-gears-installer to add the data of the original game." << std::endl;
+        }
+        boost::system::error_code ec;
+        bfs::current_path(data_directory.directory, ec);
+        if (ec){
+            std::cerr << "V-Gears can't start: can't use the data directory " << data_directory.directory.string()
+              << ": " << ec.message() << std::endl;
+            return false;
+        }
+        std::cout << "Data directory: " << data_directory.directory.string() << std::endl;
         return true;
         return true;
     }
     }
 
 

+ 22 - 0
src/common/VGearsApplication.h

@@ -118,6 +118,9 @@ namespace VGears{
             /**
             /**
              * Processes command line arguments.
              * Processes command line arguments.
              *
              *
+             * It also decides the data directory, copies the V-Gears files there if it has none (see
+             * {@see DataDirectory::Resolve}) and makes it the working directory.
+             *
              * @param[in] argc Number of command line arguments.
              * @param[in] argc Number of command line arguments.
              * @param[in] argv List of command line arguments.
              * @param[in] argv List of command line arguments.
              * @return True if all arguments were successfully processed, false
              * @return True if all arguments were successfully processed, false
@@ -131,6 +134,15 @@ namespace VGears{
              */
              */
             void registerArchiveFactories(void);
             void registerArchiveFactories(void);
 
 
+            /**
+             * Loads the Ogre plugins listed in the plugins file.
+             *
+             * Each plugin is looked for in the directories given by {@see OgrePlugins::SearchDirectories}.
+             *
+             * @throws std::runtime_error If the plugins file can't be read or a plugin is not found.
+             */
+            void loadPlugins(void);
+
             /**
             /**
              * Loads the resource configuration from  the file.
              * Loads the resource configuration from  the file.
              */
              */
@@ -223,6 +235,16 @@ namespace VGears{
              */
              */
             static const char* CLI_RESOURCES_FILE_DESCRIPTION;
             static const char* CLI_RESOURCES_FILE_DESCRIPTION;
 
 
+            /**
+             * String for the command line arguments.
+             */
+            static const char* CLI_DATA_DIR;
+
+            /**
+             * String for the command line help text.
+             */
+            static const char* CLI_DATA_DIR_DESCRIPTION;
+
             /**
             /**
              * Number of arguments assed by command line.
              * Number of arguments assed by command line.
              */
              */

+ 5 - 6
src/installer/CMakeLists.txt

@@ -169,9 +169,8 @@ if(OGRE_LIBRARIES)
 endif()
 endif()
 
 
 
 
-# Install v-gears-installer.
-if(WIN32 OR APPLE)
-    install(TARGETS v-gears-installer DESTINATION .)
-else()
-    install(TARGETS v-gears-installer RUNTIME DESTINATION bin)
-endif()
+# Install v-gears-installer (see CMake/InstallDirs.cmake).
+install(
+    TARGETS v-gears-installer
+    RUNTIME DESTINATION ${VGEARS_INSTALL_BINDIR} BUNDLE DESTINATION ${VGEARS_INSTALL_BINDIR}
+)

+ 33 - 76
src/installer/MainWindow.cpp

@@ -37,6 +37,9 @@
 #include <QtCore/QFileInfo>
 #include <QtCore/QFileInfo>
 #include <QtCore/QStorageInfo>
 #include <QtCore/QStorageInfo>
 #include <chrono>
 #include <chrono>
+#include <OgrePrerequisites.h>
+#include "common/DataDirectory.h"
+#include "common/OgrePlugins.h"
 #include "DataInstaller.h"
 #include "DataInstaller.h"
 #include "ExternalTools.h"
 #include "ExternalTools.h"
 #include "InstallLayout.h"
 #include "InstallLayout.h"
@@ -96,78 +99,39 @@ static bool installer_created = false;
  * @return Path of the folder, or an empty string if it can't be found.
  * @return Path of the folder, or an empty string if it can't be found.
  */
  */
 static QString FindGameDataDir(){
 static QString FindGameDataDir(){
-    const QString app = QCoreApplication::applicationDirPath();
-    const QStringList candidates = {
-      app + "/data", app + "/../data", app + "/../../data", app + "/../share/v-gears",
-      "/usr/share/v-gears", QDir::currentPath() + "/data", QDir::currentPath() + "/../data"
-    };
-    for (const QString& c : candidates)
-        if (QDir(c + "/data").exists() && QFile::exists(c + "/plugins.cfg")) return QDir::cleanPath(c);
-    return QString();
+    // The same places the game copies its files from when the data directory is empty, then the current directory.
+    std::vector<boost::filesystem::path> candidates = VGears::DataDirectory::TemplateCandidates(
+      boost::filesystem::path(QCoreApplication::applicationDirPath().toStdString())
+    );
+    candidates.emplace_back((QDir::currentPath() + "/data").toStdString());
+    candidates.emplace_back((QDir::currentPath() + "/../data").toStdString());
+    const boost::filesystem::path found = VGears::DataDirectory::FindTemplate(candidates, boost::filesystem::path());
+    return found.empty() ? QString() : QDir::cleanPath(QString::fromStdString(found.string()));
 }
 }
 
 
-/**
- * Directories where the system's Ogre plugins are usually installed.
- */
-static const QStringList kSystemPluginDirs = {
-  "/usr/lib/x86_64-linux-gnu/OGRE", "/usr/lib/OGRE", "/usr/lib64/OGRE", "/usr/local/lib/OGRE",
-  "/usr/lib/aarch64-linux-gnu/OGRE"
-};
-
-/**
- * Gets the names of the plugins enabled in a plugins.cfg file.
- */
-static QStringList ListOgrePlugins(const QString& cfg_path){
-    QStringList plugins;
-    QFile cfg(cfg_path);
-    if (!cfg.open(QIODevice::ReadOnly | QIODevice::Text)) return plugins;
-    while (!cfg.atEnd()){
-        const QString line = QString::fromUtf8(cfg.readLine()).trimmed();
-        if (line.startsWith("Plugin=") && !line.mid(7).trimmed().isEmpty()) plugins << line.mid(7).trimmed();
-    }
-    return plugins;
-}
+#ifndef VGEARS_OGRE_PLUGIN_DIR
+#define VGEARS_OGRE_PLUGIN_DIR ""
+#endif
 
 
 /**
 /**
- * Finds a plugin library in a directory.
+ * Names of the plugins in the plugins.cfg of a folder that the game won't find.
  *
  *
- * @return Directory-relative file names of the library (it can have several names), empty if not found.
- */
-static QStringList FindPluginFiles(const QString& dir, const QString& name){
-    return QDir(dir).entryList({name + ".so*", name + ".dll", name + ".dylib"}, QDir::Files);
-}
-
-/**
- * Names of the plugins in the plugins.cfg of a folder that are neither in it nor in the system's Ogre folders.
+ * The game looks for them in the same places (see VGears::OgrePlugins::SearchDirectories), so they don't need to be
+ * copied to the installation directory.
  */
  */
 static QStringList MissingOgrePlugins(const QString& source_dir){
 static QStringList MissingOgrePlugins(const QString& source_dir){
+    namespace bfs = boost::filesystem;
     QStringList missing;
     QStringList missing;
-    for (const QString& name : ListOgrePlugins(source_dir + "/plugins.cfg")){
-        bool found = !FindPluginFiles(source_dir, name).isEmpty();
-        for (const QString& dir : kSystemPluginDirs) found = found || !FindPluginFiles(dir, name).isEmpty();
-        if (!found) missing << name;
-    }
-    return missing;
-}
-
-/**
- * Copies the Ogre plugins listed in the installation's plugins.cfg that are not already present there (for example
- * the stock plugins shipped with the system's Ogre) from the system's Ogre plugin directory.
- *
- * @return Names of the plugins that couldn't be found anywhere.
- */
-static QStringList CopyMissingOgrePlugins(const QString& install_dir){
-    QStringList missing;
-    for (const QString& name : ListOgrePlugins(install_dir + "plugins.cfg")){
-        if (!FindPluginFiles(install_dir, name).isEmpty()) continue;
-        bool found = false;
-        for (const QString& dir : kSystemPluginDirs){
-            for (const QString& f : FindPluginFiles(dir, name)){
-                if (QFile::copy(dir + "/" + f, install_dir + f)) found = true;
-            }
-            if (found) break;
-        }
-        if (!found) missing << name;
+    const bfs::path dir(source_dir.toStdString());
+    VGears::OgrePlugins::Config config;
+    if (!VGears::OgrePlugins::ReadConfig(dir / "plugins.cfg", config)) return missing;
+    const std::vector<bfs::path> directories = VGears::OgrePlugins::SearchDirectories(
+      dir, config.folder, bfs::path(QCoreApplication::applicationDirPath().toStdString()), VGEARS_OGRE_PLUGIN_DIR
+    );
+    const std::string version = std::to_string(OGRE_VERSION_MAJOR) + "." + std::to_string(OGRE_VERSION_MINOR) + "."
+      + std::to_string(OGRE_VERSION_PATCH);
+    for (const std::string& name : config.plugins){
+        if (VGears::OgrePlugins::Find(name, directories, version).empty()) missing << QString::fromStdString(name);
     }
     }
     return missing;
     return missing;
 }
 }
@@ -500,7 +464,8 @@ void MainWindow::LaunchGame(const QString& dir){
     settings_->setValue("VGearsEXE", exe);
     settings_->setValue("VGearsEXE", exe);
     // The game runs from the installation directory, where it finds its data.
     // The game runs from the installation directory, where it finds its data.
     QStringList args;
     QStringList args;
-    args << QString("--resources-file=%1/resources.cfg").arg(dir) << QString("--config-file=%1/v-gears.cfg").arg(dir)
+    args << QString("--data-dir=%1").arg(dir)
+         << QString("--resources-file=%1/resources.cfg").arg(dir) << QString("--config-file=%1/v-gears.cfg").arg(dir)
          << QString("--plugins-file=%1/plugins.cfg").arg(dir);
          << QString("--plugins-file=%1/plugins.cfg").arg(dir);
     if (!QProcess::startDetached(exe, args, dir)){
     if (!QProcess::startDetached(exe, args, dir)){
         QMessageBox::critical(this, tr("Launch game"), tr("Could not start %1.").arg(exe));
         QMessageBox::critical(this, tr("Launch game"), tr("Could not start %1.").arg(exe));
@@ -751,19 +716,11 @@ void MainWindow::on_btn_data_run_clicked(){
                 );
                 );
                 return;
                 return;
             }
             }
-            const QStringList missing_plugins = CopyMissingOgrePlugins(output);
-            if (!missing_plugins.isEmpty()){
-                QMessageBox::critical(
-                  this, tr("Missing Ogre plugins"),
-                  tr("These Ogre plugins are not in the installation directory nor in the system:\n\n%1")
-                    .arg(missing_plugins.join(", "))
-                );
-                return;
-            }
             main_window_->data_log->append(tr("Data files copied."));
             main_window_->data_log->append(tr("Data files copied."));
         }
         }
-        // The installer's Ogre setup loads plugins.cfg, resources.cfg and the render plugin from the working
-        // directory (and writes install.log there), so run from the installation directory.
+        // The installer's Ogre setup loads plugins.cfg and resources.cfg from the working directory (and writes
+        // install.log there), so run from the installation directory. The plugins themselves are looked for in the
+        // same places as the game does (see VGears::OgrePlugins::SearchDirectories).
         if (!QFile::exists(output + "plugins.cfg") || !QFile::exists(output + "resources.cfg")){
         if (!QFile::exists(output + "plugins.cfg") || !QFile::exists(output + "resources.cfg")){
             QMessageBox::critical(
             QMessageBox::critical(
               this, tr("Missing configuration files"),
               this, tr("Missing configuration files"),

+ 7 - 1
test/CMakeLists.txt

@@ -281,8 +281,13 @@ target_link_libraries(v-gears-tests
 add_test(NAME v-gears-tests COMMAND v-gears-tests)
 add_test(NAME v-gears-tests COMMAND v-gears-tests)
 
 
 
 
-# Tests of the installer logic that doesn't need the rest of the engine (layout, option rules, verifier, log).
+# Tests of the installer logic that doesn't need the rest of the engine (layout, option rules, verifier, log), and of
+# the data directory lookup shared by the game and the installer.
 add_executable(v-gears-installer-tests
 add_executable(v-gears-installer-tests
+    common/DataDirectory.cpp
+    common/OgrePlugins.cpp
+    ${CMAKE_SOURCE_DIR}/src/common/DataDirectory.cpp
+    ${CMAKE_SOURCE_DIR}/src/common/OgrePlugins.cpp
     installer/InstallLogic.cpp
     installer/InstallLogic.cpp
     installer/ExternalTools.cpp
     installer/ExternalTools.cpp
     installer/MovieCamera.cpp
     installer/MovieCamera.cpp
@@ -296,4 +301,5 @@ add_executable(v-gears-installer-tests
 )
 )
 SET_PROPERTY(TARGET v-gears-installer-tests PROPERTY FOLDER "build/v-gears-test")
 SET_PROPERTY(TARGET v-gears-installer-tests PROPERTY FOLDER "build/v-gears-test")
 target_link_libraries(v-gears-installer-tests ${_test_qt_libs} ${Boost_LIBRARIES})
 target_link_libraries(v-gears-installer-tests ${_test_qt_libs} ${Boost_LIBRARIES})
+target_compile_definitions(v-gears-installer-tests PRIVATE VGEARS_SOURCE_DIR="${CMAKE_SOURCE_DIR}")
 add_test(NAME v-gears-installer-tests COMMAND v-gears-installer-tests)
 add_test(NAME v-gears-installer-tests COMMAND v-gears-installer-tests)

+ 183 - 0
test/common/DataDirectory.cpp

@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include <boost/test/unit_test.hpp>
+#include <boost/filesystem.hpp>
+#include <algorithm>
+#include <fstream>
+#include <sstream>
+#include "common/DataDirectory.h"
+
+namespace fs = boost::filesystem;
+namespace dd = VGears::DataDirectory;
+
+namespace{
+
+    /**
+     * A temporary directory that is removed on destruction.
+     */
+    struct Sandbox{
+        fs::path root;
+        Sandbox(): root(fs::temp_directory_path() / fs::unique_path("vgears-datadir-%%%%-%%%%")){
+            fs::create_directories(root);
+        }
+        ~Sandbox(){
+            boost::system::error_code ec;
+            fs::remove_all(root, ec);
+        }
+        void Write(const fs::path& file, const std::string& content) const{
+            fs::create_directories(file.parent_path());
+            std::ofstream(file.string()) << content;
+        }
+        std::string Read(const fs::path& file) const{
+            std::ifstream in(file.string());
+            std::stringstream text;
+            text << in.rdbuf();
+            return text.str();
+        }
+        /**
+         * Creates a template with the V-Gears files under root/name.
+         */
+        fs::path MakeTemplate(const std::string& name) const{
+            const fs::path dir = root / name;
+            Write(dir / "plugins.cfg", "template plugins");
+            Write(dir / "resources.cfg", "template resources");
+            Write(dir / "data" / "config" / "config.cfg", "template config");
+            Write(dir / "data" / "scripts" / "menu" / "begin.lua", "template script");
+            return dir;
+        }
+    };
+}
+
+BOOST_AUTO_TEST_SUITE(DataDirectoryTests)
+
+BOOST_AUTO_TEST_CASE(IsDataDirectoryNeedsPluginsAndData){
+    Sandbox box;
+    BOOST_CHECK(!dd::IsDataDirectory(box.root));
+    box.Write(box.root / "plugins.cfg", "");
+    BOOST_CHECK(!dd::IsDataDirectory(box.root));
+    fs::create_directories(box.root / "data");
+    BOOST_CHECK(dd::IsDataDirectory(box.root));
+    BOOST_CHECK(!dd::IsDataDirectory(box.root / "missing"));
+}
+
+BOOST_AUTO_TEST_CASE(RequestedDirectoryWins){
+    Sandbox box;
+    const fs::path current = box.MakeTemplate("current");
+    const fs::path requested = box.MakeTemplate("requested");
+    const dd::Resolution r = dd::Resolve(requested.string(), current, box.root / "default", {});
+    BOOST_CHECK_EQUAL(r.error, "");
+    BOOST_CHECK_EQUAL(r.directory, requested);
+    BOOST_CHECK(!r.seeded);
+}
+
+BOOST_AUTO_TEST_CASE(RelativeRequestIsRelativeToCurrent){
+    Sandbox box;
+    const fs::path requested = box.MakeTemplate("requested");
+    const dd::Resolution r = dd::Resolve("requested", box.root, box.root / "default", {});
+    BOOST_CHECK_EQUAL(r.error, "");
+    BOOST_CHECK_EQUAL(r.directory, requested);
+}
+
+BOOST_AUTO_TEST_CASE(CurrentDirectoryIsUsedIfItHasTheFiles){
+    Sandbox box;
+    const fs::path current = box.MakeTemplate("current");
+    const dd::Resolution r = dd::Resolve("", current, box.root / "default", {box.MakeTemplate("template")});
+    BOOST_CHECK_EQUAL(r.error, "");
+    BOOST_CHECK_EQUAL(r.directory, current);
+    BOOST_CHECK(!r.seeded);
+    BOOST_CHECK(!fs::exists(box.root / "default"));
+}
+
+BOOST_AUTO_TEST_CASE(DefaultDirectoryIsSeededFromTheFirstValidTemplate){
+    Sandbox box;
+    const fs::path not_a_template = box.root / "empty";
+    fs::create_directories(not_a_template);
+    const fs::path source = box.MakeTemplate("template");
+    const fs::path second = box.MakeTemplate("second");
+    box.Write(second / "plugins.cfg", "second plugins");
+    const fs::path target = box.root / "home" / ".v-gears";
+
+    const dd::Resolution r = dd::Resolve("", box.root / "elsewhere", target, {not_a_template, source, second});
+    BOOST_CHECK_EQUAL(r.error, "");
+    BOOST_CHECK_EQUAL(r.directory, target);
+    BOOST_CHECK(r.seeded);
+    BOOST_CHECK_EQUAL(r.template_directory, source);
+    BOOST_CHECK_EQUAL(r.copied_files, 4u);
+    BOOST_CHECK(dd::IsDataDirectory(target));
+    BOOST_CHECK_EQUAL(box.Read(target / "plugins.cfg"), "template plugins");
+    BOOST_CHECK_EQUAL(box.Read(target / "data" / "scripts" / "menu" / "begin.lua"), "template script");
+}
+
+BOOST_AUTO_TEST_CASE(SeedingKeepsExistingFiles){
+    Sandbox box;
+    const fs::path source = box.MakeTemplate("template");
+    // An installation where the installer extracted the game data but the configuration was not copied.
+    const fs::path target = box.root / "install";
+    box.Write(target / "data" / "config" / "config.cfg", "user config");
+    box.Write(target / "data" / "fields" / "md1_1.xml", "game data");
+
+    const dd::Resolution r = dd::Resolve(target.string(), box.root, box.root / "default", {source});
+    BOOST_CHECK_EQUAL(r.error, "");
+    BOOST_CHECK(r.seeded);
+    BOOST_CHECK_EQUAL(r.copied_files, 3u);
+    BOOST_CHECK_EQUAL(box.Read(target / "data" / "config" / "config.cfg"), "user config");
+    BOOST_CHECK_EQUAL(box.Read(target / "data" / "fields" / "md1_1.xml"), "game data");
+    BOOST_CHECK_EQUAL(box.Read(target / "plugins.cfg"), "template plugins");
+}
+
+BOOST_AUTO_TEST_CASE(UnrelatedNonEmptyDirectoryIsNotFilled){
+    Sandbox box;
+    const fs::path source = box.MakeTemplate("template");
+    const fs::path target = box.root / "documents";
+    box.Write(target / "letter.txt", "hello");
+    const dd::Resolution r = dd::Resolve(target.string(), box.root, box.root / "default", {source});
+    BOOST_CHECK(!r.error.empty());
+    BOOST_CHECK(r.directory.empty());
+    BOOST_CHECK(!fs::exists(target / "plugins.cfg"));
+}
+
+BOOST_AUTO_TEST_CASE(MissingTemplateIsAnError){
+    Sandbox box;
+    const dd::Resolution r = dd::Resolve("", box.root, box.root / "default", {box.root / "nowhere"});
+    BOOST_CHECK(!r.error.empty());
+    BOOST_CHECK(r.directory.empty());
+}
+
+BOOST_AUTO_TEST_CASE(EmptyDefaultIsAnError){
+    Sandbox box;
+    const dd::Resolution r = dd::Resolve("", box.root, fs::path(), {box.MakeTemplate("template")});
+    BOOST_CHECK(!r.error.empty());
+}
+
+BOOST_AUTO_TEST_CASE(TemplateIsNeverTheTargetItself){
+    Sandbox box;
+    const fs::path source = box.MakeTemplate("template");
+    BOOST_CHECK(dd::FindTemplate({source}, source).empty());
+    BOOST_CHECK_EQUAL(dd::FindTemplate({source}, box.root / "other"), source);
+}
+
+BOOST_AUTO_TEST_CASE(TemplateCandidatesCoverBuildTreeAndPackage){
+    const fs::path exe("/opt/repo/build/bin");
+    const std::vector<fs::path> c = dd::TemplateCandidates(exe);
+    auto has = [&](const fs::path& p){return std::find(c.begin(), c.end(), p) != c.end();};
+    BOOST_CHECK(has("/opt/repo/build/bin/data"));
+    BOOST_CHECK(has("/opt/repo/data"));
+    BOOST_CHECK(has("/opt/repo/build/share/v-gears"));
+    const std::vector<fs::path> installed = dd::TemplateCandidates("/usr/games");
+    BOOST_CHECK(std::find(installed.begin(), installed.end(), fs::path("/usr/share/v-gears")) != installed.end());
+}
+
+BOOST_AUTO_TEST_SUITE_END()

+ 138 - 0
test/common/OgrePlugins.cpp

@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include <boost/test/unit_test.hpp>
+#include <boost/filesystem.hpp>
+#include <algorithm>
+#include <fstream>
+#include "common/OgrePlugins.h"
+
+namespace fs = boost::filesystem;
+namespace op = VGears::OgrePlugins;
+
+namespace{
+
+    const std::string VERSION("1.12.10");
+
+    struct Sandbox{
+        fs::path root;
+        Sandbox(): root(fs::temp_directory_path() / fs::unique_path("vgears-plugins-%%%%-%%%%")){
+            fs::create_directories(root);
+        }
+        ~Sandbox(){
+            boost::system::error_code ec;
+            fs::remove_all(root, ec);
+        }
+        fs::path Write(const fs::path& file, const std::string& content = "x") const{
+            fs::create_directories(file.parent_path());
+            std::ofstream(file.string()) << content;
+            return file;
+        }
+        /**
+         * Creates the file of a plugin, with the name this system uses.
+         */
+        fs::path Plugin(const fs::path& directory, const std::string& name) const{
+            return Write(directory / op::FileNames(name, VERSION).front());
+        }
+    };
+
+    size_t IndexOf(const std::vector<fs::path>& list, const fs::path& item){
+        return std::find(list.begin(), list.end(), item.lexically_normal()) - list.begin();
+    }
+}
+
+BOOST_AUTO_TEST_SUITE(OgrePluginsTests)
+
+BOOST_AUTO_TEST_CASE(ReadConfigSkipsCommentsAndBlanks){
+    Sandbox box;
+    const fs::path file = box.Write(
+      box.root / "plugins.cfg",
+      "# Defines plugins to load\n\n# PluginFolder=/nowhere\n PluginFolder = plugins \r\n"
+      "# Plugin=RenderSystem_Direct3D9\n Plugin=RenderSystem_GL\r\nPlugin=\nPlugin=Codec_STBI\n"
+    );
+    op::Config config;
+    BOOST_REQUIRE(op::ReadConfig(file, config));
+    BOOST_CHECK_EQUAL(config.folder, "plugins");
+    BOOST_REQUIRE_EQUAL(config.plugins.size(), 2u);
+    BOOST_CHECK_EQUAL(config.plugins[0], "RenderSystem_GL");
+    BOOST_CHECK_EQUAL(config.plugins[1], "Codec_STBI");
+}
+
+BOOST_AUTO_TEST_CASE(ReadConfigFailsForMissingFile){
+    Sandbox box;
+    op::Config config;
+    BOOST_CHECK(!op::ReadConfig(box.root / "missing.cfg", config));
+}
+
+BOOST_AUTO_TEST_CASE(TheRepositoryConfigListsThePlugins){
+    op::Config config;
+    BOOST_REQUIRE(op::ReadConfig(fs::path(VGEARS_SOURCE_DIR) / "data" / "plugins.cfg", config));
+    BOOST_CHECK_EQUAL(config.folder, "");
+    BOOST_CHECK(std::find(config.plugins.begin(), config.plugins.end(), "RenderSystem_GL") != config.plugins.end());
+    BOOST_CHECK(std::find(config.plugins.begin(), config.plugins.end(), "Codec_STBI") != config.plugins.end());
+}
+
+BOOST_AUTO_TEST_CASE(SearchOrder){
+    const fs::path data("/home/user/.v-gears");
+    const fs::path exe("/opt/v-gears/bin");
+    const std::vector<fs::path> dirs = op::SearchDirectories(data, "plugins", exe, "/build/ogre/plugins");
+    const size_t folder = IndexOf(dirs, data / "plugins");
+    const size_t config = IndexOf(dirs, data);
+    const size_t executable = IndexOf(dirs, exe);
+    const size_t bundle = IndexOf(dirs, "/opt/v-gears/lib/OGRE");
+    const size_t build = IndexOf(dirs, "/build/ogre/plugins");
+    BOOST_REQUIRE(build < dirs.size());
+    BOOST_CHECK(folder < config);
+    BOOST_CHECK(config < executable);
+    BOOST_CHECK(executable < bundle);
+    BOOST_CHECK(bundle < build);
+}
+
+BOOST_AUTO_TEST_CASE(SearchDirectoriesHaveNoDuplicates){
+    const fs::path data("/home/user/.v-gears");
+    const std::vector<fs::path> dirs = op::SearchDirectories(data, ".", data, data.string());
+    BOOST_CHECK_EQUAL(std::count(dirs.begin(), dirs.end(), data), 1);
+}
+
+BOOST_AUTO_TEST_CASE(FindPrefersTheFirstDirectory){
+    Sandbox box;
+    const fs::path first = box.root / "first";
+    const fs::path second = box.root / "second";
+    box.Plugin(second, "RenderSystem_GL");
+    BOOST_CHECK_EQUAL(op::Find("RenderSystem_GL", {first, second}, VERSION), box.Plugin(second, "RenderSystem_GL"));
+    const fs::path in_first = box.Plugin(first, "RenderSystem_GL");
+    BOOST_CHECK_EQUAL(op::Find("RenderSystem_GL", {first, second}, VERSION), in_first);
+    BOOST_CHECK(op::Find("Codec_STBI", {first, second}, VERSION).empty());
+}
+
+#if !defined(_WIN32) && !defined(__APPLE__)
+BOOST_AUTO_TEST_CASE(FindIgnoresPluginsOfOtherOgreVersions){
+    Sandbox box;
+    box.Write(box.root / "RenderSystem_GL.so.1.12.9");
+    box.Write(box.root / "RenderSystem_GL.so");
+    BOOST_CHECK(op::Find("RenderSystem_GL", {box.root}, VERSION).empty());
+    box.Write(box.root / "RenderSystem_GL.so.1.12.10");
+    BOOST_CHECK_EQUAL(op::Find("RenderSystem_GL", {box.root}, VERSION), box.root / "RenderSystem_GL.so.1.12.10");
+}
+#endif
+
+BOOST_AUTO_TEST_CASE(FindAcceptsAbsolutePaths){
+    Sandbox box;
+    const fs::path file = box.Write(box.root / "custom" / "MyPlugin.so");
+    BOOST_CHECK_EQUAL(op::Find(file.string(), {}, VERSION), file);
+    BOOST_CHECK(op::Find((box.root / "nothing.so").string(), {}, VERSION).empty());
+}
+
+BOOST_AUTO_TEST_SUITE_END()

+ 9 - 11
v-gears.desktop

@@ -1,13 +1,11 @@
 [Desktop Entry]
 [Desktop Entry]
-Categories=Application;Game;
-Encoding=UTF-8
-Exec=sh -c "cd ~/.v-gears && exec /usr/games/v-gears"
-#Path=/usr/share/v-gears
-GenericName=V Gears
-Icon=v-gears.png
-Name=V Gears
-StartupNotify=true
-Terminal=false
 Type=Application
 Type=Application
-Version=0.21
-X-KDE-SubstituteUID=false
+Version=1.0
+Name=V-Gears
+GenericName=V-Gears
+Comment=Free Final Fantasy VII engine
+Exec=v-gears
+Icon=v-gears
+Terminal=false
+StartupNotify=true
+Categories=Game;RolePlaying;