Quellcode durchsuchen

Merge branch 'release/v0.3.2'

Iñigo Valentin vor 12 Stunden
Ursprung
Commit
14e4e7374f
69 geänderte Dateien mit 2381 neuen und 487 gelöschten Zeilen
  1. 27 0
      CMake/InstallDirs.cmake
  2. 91 0
      CMake/Packaging.cmake
  3. 6 19
      CMakeLists.txt
  4. 20 32
      README.md
  5. BIN
      data/RenderSystem_GL.so.1.12.10
  6. 2 2
      data/data/fonts/ffvii.xml
  7. 2 2
      data/data/fonts/ffvii_en.xml
  8. 1 1
      data/data/fonts/ffvii_menu.xml
  9. 1 1
      data/data/scripts/data.lua
  10. 52 22
      data/data/scripts/field.lua
  11. 7 7
      data/data/scripts/system.lua
  12. 4 2
      data/plugins.cfg
  13. 4 2
      data/plugins_d.cfg
  14. 0 5
      debian/changelog
  15. 0 1
      debian/compat
  16. 0 15
      debian/control
  17. 0 23
      debian/copyright
  18. 0 1
      debian/docs
  19. 0 5
      debian/install
  20. 0 3
      debian/menu
  21. 0 7
      debian/prerm
  22. 0 3
      debian/rules
  23. 0 1
      debian/source/format
  24. 108 22
      doc/BUILD.md
  25. 16 14
      doc/HISTORY.md
  26. 4 3
      doc/INSTALL.md
  27. 13 5
      doc/INSTALL_DATA.md
  28. 46 26
      doc/RUN.md
  29. 25 0
      doc/TODO.md
  30. 34 9
      src/CMakeLists.txt
  31. 3 3
      src/Version.h
  32. 200 0
      src/common/DataDirectory.cpp
  33. 167 0
      src/common/DataDirectory.h
  34. 113 0
      src/common/OgrePlugins.cpp
  35. 106 0
      src/common/OgrePlugins.h
  36. 96 7
      src/common/VGearsApplication.cpp
  37. 22 0
      src/common/VGearsApplication.h
  38. 124 18
      src/core/Background2D.cpp
  39. 113 1
      src/core/Background2D.h
  40. 25 3
      src/core/DialogsManager.cpp
  41. 14 0
      src/core/DialogsManager.h
  42. 5 0
      src/core/Entity.cpp
  43. 6 0
      src/core/Entity.h
  44. 91 26
      src/core/EntityManager.cpp
  45. 52 38
      src/core/ScriptManager.cpp
  46. 8 0
      src/core/ScriptManager.h
  47. 3 0
      src/core/ScriptManagerBinds.h
  48. 6 0
      src/core/UiManager.cpp
  49. 17 0
      src/core/UiManager.h
  50. 7 6
      src/core/XmlBackground2DFile.cpp
  51. 5 6
      src/installer/CMakeLists.txt
  52. 5 1
      src/installer/FieldDataInstaller.cpp
  53. 12 2
      src/installer/FieldTextWriter.cpp
  54. 33 76
      src/installer/MainWindow.cpp
  55. 1 1
      src/installer/WorldInstaller.cpp
  56. 79 4
      src/installer/decompiler/field/FieldCodeGenerator.cpp
  57. 52 2
      src/installer/decompiler/field/FieldDisassembler.cpp
  58. 6 0
      src/installer/decompiler/field/FieldEngine.cpp
  59. 35 0
      src/installer/decompiler/field/FieldEngine.h
  60. 38 2
      src/installer/decompiler/field/instruction/FieldBackgroundInstruction.cpp
  61. 4 0
      src/installer/decompiler/field/instruction/FieldBackgroundInstruction.h
  62. 38 12
      src/installer/decompiler/field/instruction/FieldCondJumpInstruction.cpp
  63. 45 4
      src/installer/decompiler/field/instruction/FieldModelInstruction.cpp
  64. 16 0
      src/installer/decompiler/field/instruction/FieldModelInstruction.h
  65. 34 30
      src/installer/decompiler/field/instruction/FieldPartyInstruction.cpp
  66. 7 1
      test/CMakeLists.txt
  67. 183 0
      test/common/DataDirectory.cpp
  68. 138 0
      test/common/OgrePlugins.cpp
  69. 9 11
      v-gears.desktop

+ 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)

+ 6 - 19
CMakeLists.txt

@@ -1,7 +1,7 @@
 # Minimum required CMake and project declaration
 cmake_minimum_required(VERSION 3.5)
 # Enable both C and CXX languages so C and C++ toolchains are configured.
-project(V-Gears VERSION 0.3.0 LANGUAGES C CXX)
+project(V-Gears VERSION 0.3.2 LANGUAGES C CXX)
 
 # Provide a central common include for project-wide policies and defaults.
 list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMake")
@@ -78,6 +78,9 @@ else()
     set(VGEARS_LUAJIT_LIBRARIES ${VGEARS_LUAJIT_LIBRARY})
 endif()
 
+# Where each part is installed. Set before src/, whose install rules use it.
+include(InstallDirs)
+
 # Build v-gears (and, if requested, the installer).
 add_subdirectory(src)
 
@@ -87,21 +90,5 @@ if(BUILD_TESTS)
     add_subdirectory(test)
 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)

+ 20 - 32
README.md

@@ -10,24 +10,28 @@ It's free software, and it's designed to be cross-platform. It, of course, doesn
 
 It's a continuation of [q-gears](https://github.com/q-gears/q-gears "Q-Gears on Github"), which is an enormous project started by some great people many years ago, with the same purpose as this, but which unfortunately became stalled.
 
-## Gallery
+## Current status
 
 Note that, while these are actual screenshots of V-Gears, none of the images, sprites, textures, backgrounds, etc are part of the project. They have been extracted locally from an original Final Fantasy VII game, and are in no way distributed here.
 ### Field maps:
+
+Most of the field maps are working. Some are buggy, some have their own quircks, and some can even crash the whole game, but overall, almost oll of them are playable. Most models, animations and opcodes are extracted and implemented. Background music work, and FMVs can be played
 |**Cloud model**|**Dialogs**|**Animations**|
 |--|--|--|
-|[<img src="https://v-gears.inigovalentin.com/img/screenshot/cloud.png" alt="Cloud model" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/cloud.png "Cloud model")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/dialog.png" alt="dialog" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/dialog.png "Dialogs")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/animation.png" alt="animation" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/animation.png "Animations")|
-|**Climbing**|**Mid-jump!**|**Materia**|
-|[<img src="https://v-gears.inigovalentin.com/img/screenshot/climb.png" alt="Climbing" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/climb.png "Climbing")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/midjump.png" alt="Mid-jump" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/midjump.png "Mid-jump")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/materia.png" alt="Materia" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/materia.png "Materia")|
+|[<img src="https://v-gears.inigovalentin.com/img/screenshot/cloud.png" alt="Cloud model" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/cloud.png "Cloud model")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/dialog.png" alt="dialog" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/dialog.png "Dialogs")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/animations.gif" alt="animation" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/animations.gif "Animations")|
 |**Transparencies**|**Timers**|**Choices**|
 |[<img src="https://v-gears.inigovalentin.com/img/screenshot/transparent_dialogs.png" alt="Transparency" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/transparent_dialogs.png "Transparency")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/timers.png" alt="Timers" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/timers.png "Timers")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/choices.png" alt="Choices" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/choices.png "Choices")|
 
 ### Battle:
-|**Battle UI**|**Battle Flow**|
-|--|--|
-|[<img src="https://v-gears.inigovalentin.com/img/screenshot/battle.png" alt="Battle UI" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/battle.png "Battle UI")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/battle.gif" alt="Battle Flow" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/battle.gif "Battle Flow")|
+Battles are... playable. Battle models and animations are implemented. The battle commands show according to the equiped materia. Basic attacks and magic are working, and some spell effects are shown. But there is atill a lot to fix: Most formulas, a lot of commands, summons...
+**Battle Flow**|
+|--|
+[<img src="https://v-gears.inigovalentin.com/img/screenshot/battle.gif" alt="Battle Flow" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/battle.gif "Battle Flow")|
 
 ### Menus:
+
+All menus are implemented and working. They behave exactly like the originals. 
+
 |**Begin**|**Main**|**Name**|
 |--|--|--|
 |[<img src="https://v-gears.inigovalentin.com/img/screenshot/begin_menu.png" alt="Begin Menu" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/begin_menu.png "Begin Menu")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/main_menu.png" alt="Main Menu" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/main_menu.png "Main Menu")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/name_menu.png" alt="Name Menu" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/name_menu.png "Name Menu")|
@@ -36,7 +40,16 @@ Note that, while these are actual screenshots of V-Gears, none of the images, sp
 |**Materia**|||
 |[<img src="https://v-gears.inigovalentin.com/img/screenshot/materia_menu.png" alt="Materia Menu" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/materia_menu.png "Materia Menu")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/save_menu.png" alt="Save Menu" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/save_menu.png "Save Menu")||
 
+### World map:
+
+The world map is bareli playable. The world map models are extracted and can be displayed. The world map is navigable, and most cities can be entered. Most vehicles are implemented and can be used, but there are still a number of bugs
+
+|**Worldmap**|**Dialogs**|**Animations**|
+|--|--|--|
+|[<img src="https://v-gears.inigovalentin.com/img/screenshot/world_map.png" alt="The worldmap and the vehicles" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/world_map.png "The worldmap and the vehicles")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/highwind.png" alt="The Highwind, mid-flight" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/highwind.png "The Highwind, mid-flight")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/worldmap.gif" alt="World map working" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/worldmap.gif "World map working")| 
+
 ### Installer:
+The installer is the tool that extract the data from the original game
 |**Installer**|**Advanced**|
 |--|--|
 |[<img src="https://v-gears.inigovalentin.com/img/screenshot/installer.png" alt="Installer" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/installer.png "Installer")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/installer_advanced.png" alt="Advanced options" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/installer_advanced.png "Advanced options")|
@@ -46,31 +59,6 @@ Note that, while these are actual screenshots of V-Gears, none of the images, sp
 |--|--|--|
 |[<img src="https://v-gears.inigovalentin.com/img/screenshot/debug_dialog.png" alt="Debug fields" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/debug_dialog.png "Debug Fields")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/walkmesh.png" alt="Walkmesh" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/walkmesh.png "Walkmesh")|[<img src="https://v-gears.inigovalentin.com/img/screenshot/console.png" alt="Console" width="250"/>](https://v-gears.inigovalentin.com/img/screenshot/console.png "Console")|
 
-## What is it's current state?
-
-Well, let's call it a work in progress... of a work in progress.
-
-|**<span style='color:#22aa22;'>Things that work</span>**|**<span style='color:#aa2222;'>Things that don't</span>**|
-|:--:|:--:|
-|A few field maps|Most field maps|
-|Some menus|Some other menus|
-|Sound (mostly)|Battle mechanics.|
-|Saving / Loading|The world map, also in general|
-|Timers|Minigames|
-| |Party switching|
-| |Shops|
-| |Alternate controllable characters|
-| |Many other things|
-
-
-If the question was "How deep into the game can I play?", then the answer is
-
-[<img src="https://v-gears.inigovalentin.com/img/screenshot/max_progress.png" alt="Max progress" width="450"/>](https://v-gears.inigovalentin.com/img/screenshot/max_progress.png "Max progress")
-
-Until there. Whitout cheats, and without using the developer console, it's not possible to get to the next wagon. The dialogs start looping there.
-
-But don't worry! The engine is not fixed sequentially, or map by map. Once a fix is implememnted, it applies to all maps at once, anb big chunck of the game are unlocked after each fix.
-
 ## In the meantime...
 
 If it picked your curiosity, you can check out the [history](doc/HISTORY.md "V-Gears history") behind the project. Or, if you want to try it for yourself, maybe the [build](doc/BUILD.md "Building guide") and [installaton](doc/INSTALL.md "Installation guide") guides are a good place to start.

BIN
data/RenderSystem_GL.so.1.12.10


+ 2 - 2
data/data/fonts/ffvii.xml

@@ -5,7 +5,7 @@
     <char name="#" x="36" y="0" width="12" height="12" pre="0" post="-2" />
     <char name="$" x="48" y="0" width="12" height="12" pre="0" post="-5" />
     <char name="%" x="60" y="0" width="12" height="12" pre="0" post="-2" />
-    <char name="&" x="72" y="0" width="12" height="12" pre="0" post="-3" />
+    <char name="&amp;" x="72" y="0" width="12" height="12" pre="0" post="-3" />
     <char name="'" x="84" y="0" width="12" height="12" pre="0" post="-9" />
     <char name="(" x="96" y="0" width="12" height="12" pre="2" post="-4" />
     <char name=")" x="108" y="0" width="12" height="12" pre="2" post="-4" />
@@ -27,7 +27,7 @@
     <char name="9" x="48" y="12" width="12" height="12" pre="0" post="-4" />
     <char name=":" x="60" y="12" width="12" height="12" pre="2" post="-7" />
     <char name=";" x="72" y="12" width="12" height="12" pre="0" post="-8" />
-    <char name="<" x="84" y="12" width="12" height="12" pre="0" post="-5" />
+    <char name="&lt;" x="84" y="12" width="12" height="12" pre="0" post="-5" />
     <char name="=" x="96" y="12" width="12" height="12" pre="0" post="-4" />
     <char name=">" x="108" y="12" width="12" height="12" pre="0" post="-5" />
     <char name="?" x="120" y="12" width="12" height="12" pre="1" post="-5" />

+ 2 - 2
data/data/fonts/ffvii_en.xml

@@ -5,7 +5,7 @@
     <char name="#" x="36" y="0" width="12" height="12" pre="0" post="-2" />
     <char name="$" x="48" y="0" width="12" height="12" pre="0" post="-5" />
     <char name="%" x="60" y="0" width="12" height="12" pre="0" post="-2" />
-    <char name="&" x="72" y="0" width="12" height="12" pre="0" post="-3" />
+    <char name="&amp;" x="72" y="0" width="12" height="12" pre="0" post="-3" />
     <char name="'" x="84" y="0" width="12" height="12" pre="0" post="-9" />
     <char name="(" x="96" y="0" width="12" height="12" pre="2" post="-4" />
     <char name=")" x="108" y="0" width="12" height="12" pre="2" post="-4" />
@@ -27,7 +27,7 @@
     <char name="9" x="48" y="12" width="12" height="12" pre="0" post="-4" />
     <char name=":" x="60" y="12" width="12" height="12" pre="2" post="-9" />
     <char name=";" x="72" y="12" width="12" height="12" pre="0" post="-8" />
-    <char name="<" x="84" y="12" width="12" height="12" pre="0" post="-5" />
+    <char name="&lt;" x="84" y="12" width="12" height="12" pre="0" post="-5" />
     <char name="=" x="96" y="12" width="12" height="12" pre="0" post="-4" />
     <char name=">" x="108" y="12" width="12" height="12" pre="0" post="-5" />
     <char name="?" x="120" y="12" width="12" height="12" pre="0" post="-6" />

+ 1 - 1
data/data/fonts/ffvii_menu.xml

@@ -162,7 +162,7 @@
     <char name="%" x="24" y="160" width="8" height="16" pre="0" post="0" />
     <char name="/" x="32" y="160" width="8" height="16" pre="0" post="0" />
     <char name=":" x="40" y="160" width="8" height="16" pre="0" post="0" />
-    <char name="&" x="48" y="160" width="8" height="16" pre="0" post="0" />
+    <char name="&amp;" x="48" y="160" width="8" height="16" pre="0" post="0" />
     <char name="【" x="56" y="160" width="8" height="16" pre="0" post="0" />
     <char name="】" x="64" y="160" width="8" height="16" pre="0" post="0" />
     <char name="「" x="104" y="160" width="8" height="16" pre="0" post="0" />

+ 1 - 1
data/data/scripts/data.lua

@@ -87,7 +87,7 @@ end
 --   get_item_quantity(item)
 --   add_money(amount)
 --   remove_money(amount)
---   money_to_2_banks(b1, b2, a1, a2)
+--   money_to_banks(b1, b2, a1, a2)
 --   sort(criteria)
 Inventory = {}
 Inventory.money = 0

+ 52 - 22
data/data/scripts/field.lua

@@ -52,34 +52,45 @@ dispatch_menu = function(menu_id, parameter)
     end
 end
 
+--- Makes an entity the player entity and places it where the previous map sent it to.
+--
+-- This is the entry point of the map (when it came from another field) or the field entry of the
+-- world map. Fields that don't have a PC opcode for the player need it too, or the player would
+-- stay at the origin of the field, out of its walkmesh.
+--
+-- @param entity_name The name of the entity that the player controls.
+place_player_at_map_entry = function(entity_name)
+    if System.MapChanger.point_name ~= "" then
+        local point = entity_manager:get_entity_point(System.MapChanger.point_name)
+        if point ~= nil then
+            local x, y, z = point:get_position()
+            local rotation = point:get_rotation()
+            local player = entity_manager:get_entity(entity_name)
+            player:set_position(x, y, z)
+            player:set_rotation(rotation)
+            player:set_solid(true)
+            player:set_visible(true)
+        end
+    end
+
+    entity_manager:set_player_entity(entity_name)
+    local world_map_entry = System.MapChanger.world_map_entry
+    if world_map_entry then
+        entity_manager:place_player_at_field_entry(
+            world_map_entry.x, world_map_entry.y,
+            world_map_entry.triangle, world_map_entry.direction
+        )
+        System.MapChanger.world_map_entry = nil
+    end
+end
+
 --- Assigns an entity to a playable character.
 --
 -- @param entity_name The entity name.
 -- @param character_id Id of the character to assign.
 set_entity_to_character = function(entity_name, character_id)
     if character_id == Party[1] and entity_name ~= "" then
-        if System.MapChanger.point_name ~= "" then
-            local point = entity_manager:get_entity_point(System.MapChanger.point_name)
-            if point ~= nil then
-                local x, y, z = point:get_position()
-                local rotation = point:get_rotation()
-                local player = entity_manager:get_entity(entity_name)
-                player:set_position(x, y, z)
-                player:set_rotation(rotation)
-                player:set_solid(true)
-                player:set_visible(true)
-            end
-        end
-
-        entity_manager:set_player_entity(entity_name)
-        local world_map_entry = System.MapChanger.world_map_entry
-        if world_map_entry then
-            entity_manager:place_player_at_field_entry(
-                world_map_entry.x, world_map_entry.y,
-                world_map_entry.triangle, world_map_entry.direction
-            )
-            System.MapChanger.world_map_entry = nil
-        end
+        place_player_at_map_entry(entity_name)
     end
     entity_manager:set_entity_to_character(entity_name, character_id)
 end
@@ -228,6 +239,25 @@ axyzi = function(bx, by, bz, bt, id, ax, ay, az, at, scale)
     return 0
 end
 
+--- Stores the X and Y position of an entity in two variables.
+--
+-- @param bx Bank for X.
+-- @param by Bank for Y.
+-- @param id Index of the entity in the field.
+-- @param ax Address at which to store the X coordinate.
+-- @param ay Address at which to store the Y coordinate.
+-- @param scale Map scale to multiply values
+getaxy = function(bx, by, id, ax, ay, scale)
+    local entity = entity_manager:get_entity_from_index(id)
+    if entity == nil then
+        do return end
+    end
+    local x, y = entity:get_position()
+    Banks[bx][ax] = math.floor(x * scale)
+    Banks[by][ay] = math.floor(y * scale)
+    return 0
+end
+
 --- Shows the ending credits.
 ending_credits = function()
     -- TODO: Implement. Probably in a looooong loooong time.

+ 7 - 7
data/data/scripts/system.lua

@@ -362,13 +362,13 @@ end
 -- destination addresses to place two two-byte values into. Address 1 takes the lower two bytes of
 -- the gil amount, while address 2 takes the higher two bytes.
 --
--- @param b1 First destination bank.
--- @param b2 Second destination address.
--- @param a1 First destination bank.
--- @param a2 Second destination address.
-Inventory.money_to_2_banks = function(b1, b2, a1, a2)
-    Banks[b1][a1] = math.floor(Inventory.money / 65535)
-    Banks[b2][a2] = math.floor(Inventory.money % 65535) - math.floor(Inventory.money / 65535)
+-- @param b1 Bank of the first destination (lower two bytes).
+-- @param b2 Bank of the second destination (higher two bytes).
+-- @param a1 Address in the first bank.
+-- @param a2 Address in the second bank.
+Inventory.money_to_banks = function(b1, b2, a1, a2)
+    Banks[b1][a1] = Inventory.money % 65536
+    Banks[b2][a2] = math.floor(Inventory.money / 65536)
 end
 
 --- Sorts the items in the inventory.

+ 4 - 2
data/plugins.cfg

@@ -1,7 +1,9 @@
 # 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
 # Plugin=RenderSystem_Direct3D9

+ 4 - 2
data/plugins_d.cfg

@@ -1,7 +1,9 @@
 # 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
 # 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)

+ 108 - 22
doc/BUILD.md

@@ -1,32 +1,18 @@
 # <img src="../v-gears.png" alt="V-Gears" width="50"/> V-Gears Building Guide
 
 ## Install dependencies
-If you are runind Debian (or derivates), you can install the required dependencies by running as root
+If you are running Debian (or derivates), you can install the required dependencies by running as root
 
 ```bash
 apt install g++ cmake libqt5widgets5 qtbase5-dev zlib1g-dev libarchive-dev libogre-1.12-dev libois-dev libvorbis-dev libboost-dev libboost-program-options-dev libboost-test-dev libboost-filesystem-dev libboost-thread-dev libopenal-dev libtinyxml-dev libluajit-5.1-dev
 ```
 
-## Deterministic fallbacks
-
-The CMake scripts include an option `USE_DETERMINISTIC_FALLBACKS` (default `ON`).
-- When `ON`, CMake will attempt pragmatic fallbacks to find libraries when
-	`find_package`/pkg-config do not succeed (SONAME `find_library` passes,
-	appending common `/usr/lib` paths, or injecting generic `-lboost_*` flags).
-- When `OFF`, CMake will not perform these fallbacks; instead require proper
-	dev packages or explicit hints (recommended for CI and packaging).
-
-Disable fallbacks example:
+The data installer also needs FFmpeg and TiMidity (see [Optional programs](#optional-programs)).
 
 ```bash
-cmake .. -DUSE_DETERMINISTIC_FALLBACKS=OFF -DCMAKE_BUILD_TYPE=Release
+apt install ffmpeg timidity
 ```
 
-If you see warnings about missing Boost or OgreBites, prefer installing the
-appropriate `-dev` packages listed above, or point CMake at your OGRE/Boost
-installation with `-DOGRE_ROOT=...` and `-DBoost_DIR=...`/`-DBoost_LIBRARY_DIRS`.
-
-
 ## Build using CMake
 
 From the project directory, run these commands:
@@ -38,18 +24,20 @@ cmake ../;
 make;
 ```
 
-If you want to speed it up a bit (it can take several minutes, depending on your system), you can add the parameter `-j` to make to use multiple threads. For instance, `make -j3` will use three cores for the compilation.
+If you want to speed it up a bit (it can take several minutes, depending on your system), you can add the parameter `-j` to make to use multiple threads. For instance, `make -j3` will use three cores for the compilation. The build uses a lot of memory, so on machines with little RAM, use a low number (`-j2`).
 
-Once the build process finishes, you u sholh have the following files (from the project directory):
+Once the build process finishes, you should have the following files (from the project directory):
 
 - `build/bin/v-gears`, the engine executable.
 - `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.
+If you build with `-DCMAKE_BUILD_TYPE=Debug`, they are in `build/bin_debug/` instead.
+
+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)
 
-The tests are not built by default. To build and run the tests of the installer logic (the option rules, the installation layout, the verifier, the log and the tool finder), which don't need the rest of the engine:
+The tests are not built by default. To build and run the tests of the installer logic (the option rules, the installation layout, the verifier, the log, the tool finder and the movie camera data) and of the data directory and Ogre plugin lookup, which don't need the rest of the engine:
 
 ```bash
 mkdir build-tests;
@@ -59,9 +47,107 @@ make v-gears-installer-tests;
 ./bin/v-gears-installer-tests
 ```
 
+The engine tests (for example the movie state and camera frames) are in the `v-gears-tests` target, built the same way.
+
+<!-- TODO(review): check that `make v-gears-tests` builds on its own and whether more build dependencies (video, audio) are required now. -->
+
+## 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` as explained in [Build using CMake](#build-using-cmake).
+
+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.
+- `share/applications/` and `share/icons/`, the desktop files and the icons.
+
+The `.deb` installs them under `/usr`, so both programs show in the applications menu. Install it with `apt`, so it installs the dependencies too:
+
+```bash
+sudo apt install ./v-gears_<version>_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. It doesn't add anything to the applications menu.
+
+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.
+
+### Packaging options
+
+These can be set when running `cmake` (`-D<option>=<value>`):
+
+|Option|Default|Description|
+|--|--|--|
+|`CMAKE_BUILD_TYPE`|`Release`|Use `Release` for packages. With `Debug`, the programs are slower and the 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, run this from the build directory:
+
+```bash
+sudo cmake --install . --prefix /usr/local
+```
+
+## Advanced build options
+
+### Deterministic fallbacks
+
+You don't need this for a normal build. It only matters if CMake can't find a library that is installed.
+
+The CMake scripts include an option `USE_DETERMINISTIC_FALLBACKS` (default `ON`).
+- When `ON`, CMake will attempt pragmatic fallbacks to find libraries when
+	`find_package`/pkg-config do not succeed (SONAME `find_library` passes,
+	appending common `/usr/lib` paths, or injecting generic `-lboost_*` flags).
+- When `OFF`, CMake will not perform these fallbacks; instead require proper
+	dev packages or explicit hints.
+
+**Leave it `ON` for now, also for packages.** 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
+appropriate `-dev` packages listed above, or point CMake at your OGRE/Boost
+installation with `-DOGRE_ROOT=...` and `-DBoost_DIR=...`/`-DBoost_LIBRARY_DIRS`.
+
 ## 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, the music and the videos, the installer uses FFmpeg and TiMidity. They are not needed to build V-Gears, only to install the data. In Debian (or derivates):
+
+```bash
+apt install ffmpeg timidity
+```
+
+See the [data installation guide](INSTALL_DATA.md "Install Data").
 
 ## Next steps
 

+ 16 - 14
doc/HISTORY.md

@@ -32,7 +32,7 @@ A couple of months later, Halkun resigned as project leader<sup>[5](#####[5]])</
 
 ---
 
-##2007 - 2012 The Project Continues.
+## 2007 - 2012 The Project Continues.
 
 Still, the project kept on growing and advancing. Akari was still doing most of the contributions, and took an unofficial role of project leader. Over the years, the engine was playable, and Final Fantasy VII could, to an extent, actually be played on Q-Gears. Work was getting done, new things were being implemented, corners were being polished.
 
@@ -42,7 +42,7 @@ Eventually, it seems Akari himself started loosing interest in the project. The
 
 ---
 
-##2015 - A New Hope.
+## 2015 - A New Hope.
 
 The project, seemingly dead, received a new impulse in 2015, thanks to paulsapps<sup>[6](#####[6]])</sup>. He was not, as far as I'm aware, a member of Qhimm Forums, but he took an interest on the project. He modernized the codebase, fixed many errors, and more importantly, developed a series of very usefull tools, the most remarkable one being SUDM<sup>[7](#####[7]])</sup>, which allowed to easily import data to Q-Gears and to reimplement field scripts.
 
@@ -50,7 +50,7 @@ That same year, apparently paulsapps started it's own fork of Q-Gears: 7-Gears<s
 
 ---
 
-##2022 - V-Gears.
+## 2022 - V-Gears.
 
 2022 is when I<sup>[9](#####[8]])</sup> found out about Q-Gears. I really loved the idea, and it made me sad that the project had died. So I started reading, trying to understand the code base and the ins and outs of the game. 
 
@@ -60,7 +60,9 @@ The next step was to modernize the code base. I made some fixes here and there,
 
 Then, I set out to do what I thougth was the number one priority, and fix what I personally thing was one of the main reasons the project had died: The lack of documentation. Slowly but steadly, I went through the entire code base, documenting every class, every function, every piece of code. I added whatever documents I had about the game formats and files. I added a project history, a usefull README, building instructions...
 
-As of yet, I have added no new functionalities to the engine itself.
+For a while, I added no new functionalities to the engine itself. That changed after the cleanup was done. Since then V-Gears has gained, among other things, a rewritten data installer (with ISO reading, verification, and FFmpeg / TiMidity detection), playback of the original videos, a shared menu library with most of the game menus, saving and loading of games and settings, and a battle system with the original battle data, models, animations and spell effects.
+
+<!-- TODO(review): the history above is a short summary of versions 0.1 to 0.3.0; expand or reword it in your own voice. -->
 
 ---
 
@@ -76,13 +78,13 @@ And maybe, one day we can play our favourite game over an engine called Somethin
 
 ### References:
 
-#####[1] Halkun profile on Qhimm Forums: [https://forums.qhimm.com/index.php?action=profile;u=109](https://forums.qhimm.com/index.php?action=profile;u=109)
-#####[2] Q-Gears presentation post on Qhimm Forums: [https://forums.qhimm.com/index.php?topic=5556.0](https://forums.qhimm.com/index.php?topic=5556.0)
-#####[3] Akari profile on Qhimm Forums: [https://forums.qhimm.com/index.php?action=profile;u=1266](https://forums.qhimm.com/index.php?action=profile;u=1266)
-#####[4] Akari's fork announcement on Qhimm Forums: [https://forums.qhimm.com/index.php?topic=6829.0](https://forums.qhimm.com/index.php?topic=6829.0)
-#####[5] Halkun's resignation: [https://forums.qhimm.com/index.php?topic=6963.0](https://forums.qhimm.com/index.php?topic=6963.0)
-#####[6] paulsapps's profile on GitHub: [https://github.com/paulsapps](https://github.com/paulsapps)
-#####[7] SUDM repository on GitHub: [https://github.com/paulsapps/SUDM/](https://github.com/paulsapps/SUDM/)
-#####[8] 7-Gears repository on GitHub: [https://github.com/paulsapps/7-Gears](https://github.com/paulsapps/7-Gears)
-#####[9] Iñigo Valentin's profile on GitHub: [https://github.com/InigoValentin](https://github.com/InigoValentin)
-#####[9] V-Gears repository on GitHub: [https://github.com/InigoValentin/V-Gears](https://github.com/InigoValentin/V-Gears)
+##### [1] Halkun profile on Qhimm Forums: [https://forums.qhimm.com/index.php?action=profile;u=109](https://forums.qhimm.com/index.php?action=profile;u=109)
+##### [2] Q-Gears presentation post on Qhimm Forums: [https://forums.qhimm.com/index.php?topic=5556.0](https://forums.qhimm.com/index.php?topic=5556.0)
+##### [3] Akari profile on Qhimm Forums: [https://forums.qhimm.com/index.php?action=profile;u=1266](https://forums.qhimm.com/index.php?action=profile;u=1266)
+##### [4] Akari's fork announcement on Qhimm Forums: [https://forums.qhimm.com/index.php?topic=6829.0](https://forums.qhimm.com/index.php?topic=6829.0)
+##### [5] Halkun's resignation: [https://forums.qhimm.com/index.php?topic=6963.0](https://forums.qhimm.com/index.php?topic=6963.0)
+##### [6] paulsapps's profile on GitHub: [https://github.com/paulsapps](https://github.com/paulsapps)
+##### [7] SUDM repository on GitHub: [https://github.com/paulsapps/SUDM/](https://github.com/paulsapps/SUDM/)
+##### [8] 7-Gears repository on GitHub: [https://github.com/paulsapps/7-Gears](https://github.com/paulsapps/7-Gears)
+##### [9] Iñigo Valentin's profile on GitHub: [https://github.com/InigoValentin](https://github.com/InigoValentin)
+##### [10] V-Gears repository on GitHub: [https://github.com/InigoValentin/V-Gears](https://github.com/InigoValentin/V-Gears)

+ 4 - 3
doc/INSTALL.md

@@ -2,7 +2,7 @@
 
 ## Manual installation
 
-The data installer can do this for you: enable "Copy V-Gears data files" in its advanced options and it copies the `data/` folder of the repository to the installation directory. To do it by hand:
+The data installer can do this for you: enable "Copy V-Gears data files" in its advanced options and it copies the `data/` folder of the repository (`share/v-gears/` in an installed package) to the installation directory. To do it by hand:
 
 1. Create a folder for V-Gears data, outside the project. In Linux-based systems, it's recommended to use `~/.v-gears/`.
 2. Copy all the contents of the `data/` folder of the repository to the newly created directory. It should end up like this:
@@ -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
   ├── plugins.cfg
   ├── plugins_d.cfg
-  ├── RenderSystem_GL.so.1.12.10
   ├── resources.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
 

+ 13 - 5
doc/INSTALL_DATA.md

@@ -8,9 +8,9 @@ If you also want the videos, you need the ISO images of the PC game disks 1, 2 a
 
 ## FFmpeg and TiMidity
 
-The installer converts the sound effects and the music to `.ogg` files with two external programs:
+The installer converts the sound effects, the music and the videos with two external programs (the videos only need FFmpeg):
 
-- [FFmpeg](https://ffmpeg.org/download.html "FFmpeg") converts the sound effects and encodes the music.
+- [FFmpeg](https://ffmpeg.org/download.html "FFmpeg") converts the sound effects, encodes the music and converts the videos.
 - [TiMidity](https://timidity.sourceforge.net/#download "TiMidity") plays the original MIDI music so FFmpeg can encode it. Only the music needs it.
 
 You don't have to configure anything if they are installed. The installer looks for them, in this order:
@@ -26,7 +26,7 @@ If a program that the installation needs is missing, the installer tells you bef
 - **Locate…** the program in your system.
 - **Get it…**: open its download page.
 - **Check again**, for when you have just installed it.
-- **Continue without** it. The rest is installed normally, and the sound effects, the music, or both are left out. The extracted `.wav` and `.mid` files stay in `~/.v-gears/data/audio/`, and the indexes only list the files that exist.
+- **Continue without** it. The rest is installed normally, and the sound effects, the music, the videos, or any of them, are left out. The extracted `.wav` and `.mid` files stay in `~/.v-gears/data/audio/`, and the indexes only list the files that exist.
 - **Cancel** the installation.
 
 To add the missing audio later, install the programs and run the installer again with only the sound effects and the music selected (the *Media only* option, or *Custom*).
@@ -45,7 +45,7 @@ It can be started from any directory. When the installation starts, it works fro
 
 1. In **Import data source**, select the ISO image of the install disk. The installer tells you if the image is valid.
 2. In **VGears installation directory**, select where to install, for example `~/.v-gears/`. It will be created if it doesn't exist. It holds the configuration and the converted data, and it is where the game runs.
-3. (Optional) Select **Show advanced options** to choose what to install (see below). The defaults install everything. If the directory doesn't have the V-Gears files yet (configuration, scripts, screens, fonts...), leave **Copy V-Gears data files** checked, and the installer copies them there from the `data/` folder of the repository.
+3. (Optional) Select **Show advanced options** to choose what to install (see below). The defaults install everything. If the directory doesn't have the V-Gears files yet (configuration, scripts, screens, fonts...), leave **Copy V-Gears data files** checked, and the installer copies them there from the `data/` folder of the repository (`share/v-gears/` in an installed package). The engine also copies them by itself the first time it runs.
 4. Click **Install data**.
 
 Before extracting anything, the installer checks that everything is in place: that the ISO is valid, that the directory is writable and has enough free space (about 3 GB plus the size of the ISO), and that the FFmpeg and TiMidity programs it needs can be used (see below). If something is wrong it tells you and doesn't start.
@@ -63,7 +63,15 @@ When it finishes:
 
 The videos are not on the install disk, but on the PC game disks 1, 2 and 3. To get them, run the installer once for each disk: select the disk's ISO as the data source and the same installation directory, and click 'Install data'. The installer detects a game disk, extracts only its videos to `<installation directory>/data/video/` and doesn't touch anything else. Several disks have the same videos, so those already installed are skipped. Unlike the full installation, it can be repeated with the same installer window.
 
-The videos are copied as they are (AVI files with the Duck TrueMotion 2 codec, which FFmpeg can decode). V-Gears doesn't play them yet.
+The engine can't decode the original AVI files (Duck TrueMotion 2 codec), so the installer uses FFmpeg to convert each video to a folder of JPEG frames, an `.ogg` sound track (if the video has sound) and a `movie.xml` with the frame count and frame rate. Movies that carry camera data for the field background also get a `camera.cam` file. They are played by the engine's movie player, which draws the frames and plays the sound. FFmpeg is therefore needed to install the videos.
+
+<!-- TODO(review): confirm the exact output layout under data/video/ (folder per movie, file names) after a fresh install, and which movies play in game. -->
+
+## Battle data
+
+The battle installation extracts the battle scenes, enemy and formation data, the attacks, the field and world map encounter tables, and the battle models and animations. It also extracts the visual assets of the spell effects (`magic.lgp`): the meshes and textures of the effects and the sprite lists, which the battle scripts use to recreate the original spell effects. If you installed before these were added, run the installer again with the battle options selected (*Battle* preset or *Custom*) to get them.
+
+<!-- TODO(review): list of spells with effects (currently the Fire, Ice, Bolt and Cure families) and the reinstall requirement should be re-checked against the current state of battle_fx.lua. -->
 
 ## Advanced options
 

+ 46 - 26
doc/RUN.md

@@ -1,34 +1,54 @@
-# <img src="../v-gears.png" alt="V-Gears" width="50"/> V-Gears Data Run Guide
+# <img src="../v-gears.png" alt="V-Gears" width="50"/> V-Gears Run Guide
 
 ## Run the engine
 
-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 [installation](INSTALL.md "Installation guide") guides, you'll have the game data installed to `~/.v-gears/`, and the binaries compiled somewhere (we'll assume 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
-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.
-
-## Key Mappings
-
-Here is a list of key mappings, adapted from the PSX version of the game:
-
-- &#129033;: Up
-- &#129034;: Right
-- &#129035;: Down
-- &#129032;: Left
-- &#10799;: ?
-- &#9633;: ?
-- &#9675;: Space bar
-- &#9651;: Esc
-- &#11207;: Enter
-- &#9644;: ?
-- **R1**: ?
-- **L1**: ?
-- **R2**: ?
-- **L2**: ?
+If you installed V-Gears from a package, run `v-gears`, or start it from the applications menu.
+
+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.
+
+## Controls
+
+The default bindings are in `config/config.cfg` of the installation directory, in the `bind_game_event` lines. The game reads them as events, so the menus, the field and the battles share them. They can be changed there.
+
+|Key|Event|Action|
+|--|--|--|
+|Arrow keys|Up, Down, Left, Right|Move, move through menus|
+|Enter, Space|CONFIRM|Confirm, talk, interact|
+|Esc|CANCEL|Cancel, go back|
+|M|MENU|Open the main menu|
+|Backspace|EXTRA|Extra action in some menus|
+|P|START|Start|
+|V|SELECT|Select|
+|Page Up / Page Down|L1 / R1|Switch tab or character in menus|
+|Left Ctrl|run|Run in the field|
+
+The settings made in the in-game config menu (controls, sound, etc.) are saved with the game settings and loaded on start.
+
+<!-- TODO(review): verify the table against the in-game config menu (control remapping), and check the gamepad status. -->
+
+## Debug keys
+
+These are also defined in `config.cfg`:
+
+- **F11**: pause / resume the game timer. **Num +** / **Num -**: change the game speed.
+- **F12**: take a screenshot.
+- **Left Ctrl + Left Alt** plus **S, F, E, T, W, G, A, B, U, M**: toggle the debug displays (script, FPS, entities, triggers, walkmesh, grid, axis, background, UI, messages).
+- **F8** / **F9**: show / hide the begin menu.
+- **Alt + F4**: quit.

+ 25 - 0
doc/TODO.md

@@ -0,0 +1,25 @@
+# TODO
+
+Notes of things found while playing through the fields that still have to be reviewed.
+
+## Long pauses between messages in chrin_1b
+
+Some scenes in chrin_1b (the opening, and the ones after it) wait a long time between messages, so
+it looks as if the game had frozen. Not investigated yet.
+
+Suspects:
+
+- The script scheduling in `ScriptManager::Update`: a script that yields with `wait(0)` now lets the
+  next script of its entity run in the same cycle.
+- The priority of the talk script, `INTERACT_SCRIPT_PRIORITY` in `EntityManager.cpp`.
+- The durations of `WAIT` and `script:wait` that the installer converts (frames to seconds).
+
+First step: play the scene with log level 3 and look at the time between the `dialog_open` and
+`Start script` lines.
+
+## Background parallax
+
+`BGPDH` and `BGSCR` are converted, but the meaning of their values (ratio = distance / 4096, speed in
+1/256 of a pixel per frame, sign of the speed) has not been checked against the original game. The far
+layer of mds5_4 has no such opcodes and still scrolls with the rest, and it is not known why it should
+not.

+ 34 - 9
src/CMakeLists.txt

@@ -593,6 +593,8 @@ set(VGEARS_SOURCE_FILES
     common/FinalFantasy7/FF7NameLookup.cpp
     common/LzsFile.cpp
     common/TypeDefine.cpp
+    common/DataDirectory.cpp
+    common/OgrePlugins.cpp
     common/VGearsApplication.cpp
     common/VGearsManualObject.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")
 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})
 target_sources(libvgears PRIVATE installer/SfxDump.c)
 # Ensure generated ui headers are created before compiling libvgears
@@ -877,6 +891,7 @@ if(DEFINED _GENERATED_UI_TARGET)
 endif()
     target_link_libraries(libvgears PUBLIC ${VGEARS_LUAJIT_LIBRARIES})
 target_compile_definitions(libvgears PUBLIC TIXML_USE_STL)
+target_compile_definitions(libvgears PUBLIC VGEARS_OGRE_PLUGIN_DIR="${VGEARS_OGRE_PLUGIN_DIR}")
 if(NOT MSVC)
     target_compile_options(libvgears PRIVATE -fPIC)
 endif()
@@ -923,8 +938,6 @@ endforeach()
 
 add_executable(v-gears ${VGEARS_SOURCE_FILES})
 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)
     target_link_libraries(v-gears "-framework CoreFoundation -framework Cocoa -framework IOKit" ${ois_lib})
 endif()
@@ -942,6 +955,22 @@ set(VGEARS_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
 # generic `-lboost_*` flags directly so the system linker will attempt to
 # resolve them from standard library search paths.
@@ -1035,18 +1064,14 @@ if(UNIX)
     target_compile_definitions(v-gears PRIVATE POSIX)
 endif()
 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)
     target_compile_options(v-gears PRIVATE -fPIC)
 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

+ 3 - 3
src/Version.h

@@ -22,8 +22,8 @@
 /**
  * Application version.
  */
-#define VGEARS_VERSION "0.3.0"
+#define VGEARS_VERSION "0.3.2"
 #define VGEARS_VERSION_MAJOR "0"
 #define VGEARS_VERSION_MINOR "3"
-#define VGEARS_VERSION_PATCH "0"
-#define VGEARS_VERSION_SIGNATURE "V-Gears v0.3.0"
+#define VGEARS_VERSION_PATCH "2"
+#define VGEARS_VERSION_SIGNATURE "V-Gears v0.3.2"

+ 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.
  */
 
+#include <cstdlib>
 #include <iostream>
 #include <boost/filesystem.hpp>
 #include <boost/program_options.hpp>
@@ -21,6 +22,8 @@
 #include <OgreLogManager.h>
 #include <OgreConfigDialog.h>
 #include "common/VGearsApplication.h"
+#include "common/DataDirectory.h"
+#include "common/OgrePlugins.h"
 #include "VGearsGameState.h"
 #include "data/VGearsAFileManager.h"
 #include "data/VGearsBackgroundFileManager.h"
@@ -44,6 +47,13 @@
  */
 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 bfs = boost::filesystem;
@@ -67,6 +77,11 @@ namespace VGears{
     const char* Application::CLI_RESOURCES_FILE_DESCRIPTION(
       "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){}
 
@@ -81,9 +96,9 @@ namespace VGears{
             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>();
 
         Ogre::Log* default_log(
@@ -119,6 +134,43 @@ namespace VGears{
         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(){
 
         // Load resource paths from config file
@@ -186,6 +238,10 @@ namespace VGears{
             (  CLI_RESOURCES_FILE
               , bpo::value< String >()->default_value(resources_filename)
               , CLI_RESOURCES_FILE_DESCRIPTION
+           )
+            (  CLI_DATA_DIR
+              , bpo::value< String >()
+              , CLI_DATA_DIR_DESCRIPTION
            )
             ;
         bpo::variables_map vm;
@@ -196,11 +252,44 @@ namespace VGears{
         }
 
         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;
     }
 

+ 22 - 0
src/common/VGearsApplication.h

@@ -118,6 +118,9 @@ namespace VGears{
             /**
              * 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] argv List of command line arguments.
              * @return True if all arguments were successfully processed, false
@@ -131,6 +134,15 @@ namespace VGears{
              */
             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.
              */
@@ -223,6 +235,16 @@ namespace VGears{
              */
             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.
              */

+ 124 - 18
src/core/Background2D.cpp

@@ -127,6 +127,11 @@ void Background2D::Update(){
         SetScroll(pending_scroll_);
     }
     UpdateShake(Timer::getSingleton().GetGameTimeDelta());
+    // The layers that scroll by themselves.
+    for (int layer = 0; layer < LAYER_COUNT; ++ layer){
+        layer_scroll_offset_[layer][0] += layer_scroll_speed_[layer][0] * Timer::getSingleton().GetGameTimeDelta();
+        layer_scroll_offset_[layer][1] += layer_scroll_speed_[layer][1] * Timer::getSingleton().GetGameTimeDelta();
+    }
     for (unsigned int i = 0; i < animation_played_.size(); ++ i){
         for (unsigned int j = 0; j < animations_.size(); ++ j){
             if (animations_[j]->GetName() == animation_played_[i].name){
@@ -623,6 +628,25 @@ void Background2D::SetRange(const int min_x, const int min_y, const int max_x, c
     calculateScreenScale();
 }
 
+float Background2D::TileDepthToBuffer(const float depth, const Ogre::Matrix4& projection){
+    Ogre::Vector4 result = projection * Ogre::Vector4(0, 0, -depth, 1);
+    result /= result.w;
+    // Starting from this depth the projected values are too close to each other for the depth
+    // buffer to tell them apart.
+    const float far_group_start = 127.0f;
+    if (depth >= far_group_start){
+        Ogre::Vector4 start = projection * Ogre::Vector4(0, 0, -far_group_start, 1);
+        start /= start.w;
+        // The depth of the render system goes from -1 to 1 (OpenGL), so between the start of the
+        // group and the far plane there are about 1.4e-5 to use. With 1e-5 per unit of depth,
+        // tiles whose depths differ by 0.06 (127.938 and 128) end up several steps of a 24 bit
+        // depth buffer apart, and the biggest depth used (128) is still in front of the far plane.
+        const float far_group_step = 0.00001f;
+        return std::min(start.z + (depth - far_group_start) * far_group_step, 0.9999995f);
+    }
+    return result.z;
+}
+
 void Background2D::AddTile(const VGears::Tile& tile){
     // TODO: move depth calculation to flevelBackgroundLoader maybe? and let
     // Backgorund2D only handle 0 <= depth <= 1 or so?
@@ -630,13 +654,10 @@ void Background2D::AddTile(const VGears::Tile& tile){
     Ogre::Real depth(0.0001f);
     if (tile.depth >= 1){
         if (tile.depth < 4095){
-            const Ogre::Matrix4 &cam_projection(
+            depth = TileDepthToBuffer(
+              tile.depth,
               CameraManager::getSingleton().GetCurrentCamera()->getProjectionMatrixWithRSDepth()
             );
-            Ogre::Vector4 res(0, 0, -tile.depth, 1);
-            res = cam_projection * res;
-            res /= res.w;
-            depth = res.z;
         }
         else depth = 0.9999f;
     }
@@ -667,7 +688,7 @@ void Background2D::virtualScreenToWorldSpace(Ogre::Vector2& pos) const{
 void Background2D::AddTile(
   const int x, const int y, const int width, const int height, const float depth, const float u1,
   const float v1, const float u2, const float v2, const Blending blending, const int area,
-  const int state
+  const int state, const int layer
 ){
     Ogre::RenderOperation render_op;
     Ogre::HardwareVertexBufferSharedPtr vertex_buffer;
@@ -698,6 +719,16 @@ void Background2D::AddTile(
         );
         return;
     }
+    // Keep where the tiles of each layer are in the buffer.
+    {
+        const int buffer = (blending == VGears::B_ALPHA) ? 0 : ((blending == VGears::B_ADD) ? 1 : 2);
+        const int clamped = (layer < 0) ? 0 : ((layer >= LAYER_COUNT) ? LAYER_COUNT - 1 : layer);
+        if (last_layer_[buffer] > clamped) layers_contiguous_[buffer] = false;
+        if (clamped > last_layer_[buffer]) last_layer_[buffer] = clamped;
+        LayerRange& range = layer_ranges_[buffer][clamped];
+        if (range.count == 0) range.first = render_op.vertexData->vertexCount;
+        range.count += TILE_VERTEX_COUNT;
+    }
     Tile tile;
     tile.x = x;
     tile.y = y;
@@ -901,23 +932,98 @@ void Background2D::renderQueueEnded(
         render_system_->_setViewMatrix(view);
         rs_params->setConstant(Ogre::GpuProgramParameters::ACT_VIEW_MATRIX, view);
         render_system_->applyFixedFunctionParams(rs_params, Ogre::GPV_GLOBAL);
-        if (alpha_render_op_.vertexData->vertexCount != 0){
-            scene_manager_->_setPass(alpha_material_->getTechnique(0)->getPass(0), true, false);
-            render_system_->_render(alpha_render_op_);
-        }
-        if (add_render_op_.vertexData->vertexCount != 0){
-            scene_manager_->_setPass(add_material->getTechnique(0)->getPass(0), true, false);
-            render_system_->_render(add_render_op_);
-        }
-        if(subtract_render_op_.vertexData->vertexCount != 0){
-            scene_manager_->_setPass(subtract_material_->getTechnique(0)->getPass(0), true, false);
-            render_system_->_render(subtract_render_op_);
-        }
+        RenderBuffer(0, alpha_render_op_, alpha_material_, rs_params, width, height);
+        RenderBuffer(1, add_render_op_, add_material, rs_params, width, height);
+        RenderBuffer(2, subtract_render_op_, subtract_material_, rs_params, width, height);
+    }
+}
+
+void Background2D::ResetLayers(){
+    for (int buffer = 0; buffer < 3; ++ buffer){
+        for (int layer = 0; layer < LAYER_COUNT; ++ layer) layer_ranges_[buffer][layer] = LayerRange();
+        last_layer_[buffer] = -1;
+        layers_contiguous_[buffer] = true;
+    }
+    for (int layer = 0; layer < LAYER_COUNT; ++ layer){
+        layer_parallax_[layer][0] = layer_parallax_[layer][1] = 1.0f;
+        layer_scroll_speed_[layer][0] = layer_scroll_speed_[layer][1] = 0.0f;
+        layer_scroll_offset_[layer][0] = layer_scroll_offset_[layer][1] = 0.0f;
+    }
+}
 
+bool Background2D::HasLayerScroll() const{
+    for (int layer = 0; layer < LAYER_COUNT; ++ layer){
+        if (
+          layer_parallax_[layer][0] != 1.0f || layer_parallax_[layer][1] != 1.0f
+          || layer_scroll_speed_[layer][0] != 0.0f || layer_scroll_speed_[layer][1] != 0.0f
+        ) return true;
+    }
+    return false;
+}
+
+void Background2D::ScriptSetLayerParallax(const int layer, const int distance){
+    if (layer < 0 || layer >= LAYER_COUNT) return;
+    const float ratio = static_cast<float>(distance) / 4096.0f;
+    layer_parallax_[layer][0] = ratio;
+    layer_parallax_[layer][1] = ratio;
+}
+
+void Background2D::ScriptSetLayerScroll(const int layer, const int x, const int y){
+    if (layer < 0 || layer >= LAYER_COUNT) return;
+    // 1/256 of a pixel of the original screen for each frame, at 30 frames per second. The
+    // virtual screen is 3 times as big as the one of the original game.
+    const float to_virtual_pixels_per_second = 30.0f * 3.0f / 256.0f;
+    layer_scroll_speed_[layer][0] = static_cast<float>(x) * to_virtual_pixels_per_second;
+    layer_scroll_speed_[layer][1] = static_cast<float>(y) * to_virtual_pixels_per_second;
+}
+
+void Background2D::RenderBuffer(
+  const int buffer, Ogre::RenderOperation& render_op, const Ogre::MaterialPtr& material,
+  Ogre::GpuProgramParametersPtr& rs_params, const float width, const float height
+){
+    if (render_op.vertexData->vertexCount == 0) return;
+    const unsigned int total = render_op.vertexData->vertexCount;
+    const bool by_layer = layers_contiguous_[buffer] && HasLayerScroll();
+    if (!by_layer){
+        // The view the layers of other buffers could have left.
+        Ogre::Matrix4 view;
+        view.makeTrans(Ogre::Vector3(position_real_.x / width, -position_real_.y / height, 0));
+        render_system_->_setViewMatrix(view);
+        rs_params->setConstant(Ogre::GpuProgramParameters::ACT_VIEW_MATRIX, view);
+        render_system_->applyFixedFunctionParams(rs_params, Ogre::GPV_GLOBAL);
+        scene_manager_->_setPass(material->getTechnique(0)->getPass(0), true, false);
+        render_system_->_render(render_op);
+        return;
+    }
+    for (int layer = 0; layer < LAYER_COUNT; ++ layer){
+        const LayerRange& range = layer_ranges_[buffer][layer];
+        if (range.count == 0) continue;
+        // The scroll of the background, changed by the parallax of the layer, plus the one it has
+        // by itself. The scroll by itself is in pixels of the virtual screen.
+        const Ogre::Vector2 by_itself(
+          layer_scroll_offset_[layer][0] * screen_scale_,
+          layer_scroll_offset_[layer][1] * screen_scale_
+        );
+        Ogre::Matrix4 view;
+        view.makeTrans(Ogre::Vector3(
+          (position_real_.x * layer_parallax_[layer][0] + by_itself.x) / width,
+          -(position_real_.y * layer_parallax_[layer][1] + by_itself.y) / height, 0
+        ));
+        // TODO This is deprecated, but if not done , the background image disappears.
+        render_system_->_setViewMatrix(view);
+        rs_params->setConstant(Ogre::GpuProgramParameters::ACT_VIEW_MATRIX, view);
+        render_system_->applyFixedFunctionParams(rs_params, Ogre::GPV_GLOBAL);
+        scene_manager_->_setPass(material->getTechnique(0)->getPass(0), true, false);
+        render_op.vertexData->vertexStart = range.first;
+        render_op.vertexData->vertexCount = range.count;
+        render_system_->_render(render_op);
+        render_op.vertexData->vertexStart = 0;
+        render_op.vertexData->vertexCount = total;
     }
 }
 
 void Background2D::CreateVertexBuffers(){
+    ResetLayers();
     alpha_max_vertex_count_ = 2048 * TILE_VERTEX_COUNT;
     alpha_render_op_.vertexData = new Ogre::VertexData;
     alpha_render_op_.vertexData->vertexStart = 0;

+ 113 - 1
src/core/Background2D.h

@@ -230,6 +230,32 @@ class Background2D : public Ogre::RenderQueueListener{
          * @param[in] y_amplitude Amplitude of the bounces in the Y axis, in original screen pixels.
          * @param[in] y_frames Frames between bounces in the Y axis.
          */
+        /**
+         * Sets the parallax of a background layer, from the distance the scripts give it.
+         *
+         * The layer scrolls at the speed of the background multiplied by the distance divided by
+         * 4096 (the distance of the layers that don't move with it is 4096), so a layer far away
+         * moves less than the foreground. That relation comes from the values the field scripts
+         * use and has not been verified against the original game.
+         *
+         * @param[in] layer Layer of the background, from 0 to 3.
+         * @param[in] distance Parallax distance of the layer.
+         */
+        void ScriptSetLayerParallax(const int layer, const int distance);
+
+        /**
+         * Sets the speed a background layer scrolls by itself, on top of the scroll of the
+         * background.
+         *
+         * The speeds are given in 1/256 of a pixel of the original screen for each frame of the
+         * original game (30 per second). That unit has not been verified against the original game.
+         *
+         * @param[in] layer Layer of the background, from 0 to 3.
+         * @param[in] x Speed on the X axis.
+         * @param[in] y Speed on the Y axis.
+         */
+        void ScriptSetLayerScroll(const int layer, const int x, const int y);
+
         void ScriptShake(
           const int type, const int x_amplitude, const int x_frames, const int y_amplitude,
           const int y_frames
@@ -363,7 +389,7 @@ class Background2D : public Ogre::RenderQueueListener{
         void AddTile(
           const int x, const int y, const int width, const int height, const float depth,
           const float u1, const float v1, const float u2, const float v2, const Blending blending,
-          const int area = 0, const int state = 0
+          const int area = 0, const int state = 0, const int layer = 0
         );
 
         /**
@@ -390,6 +416,22 @@ class Background2D : public Ogre::RenderQueueListener{
          */
         void AddTile(const VGears::Tile& tile);
 
+        /**
+         * Converts the depth of a tile to the value that is written to the depth buffer.
+         *
+         * The depth is projected with the camera. The tiles that are the farthest (the background
+         * layers) are at depths so close to each other (as 127.938 and 128) that their projected
+         * values are closer than what the depth buffer can tell apart, and the order they are
+         * drawn in decided what was in front. For them the order is kept with a linear mapping
+         * right behind all the rest of what is drawn.
+         *
+         * @param[in] depth Depth of the tile.
+         * @param[in] projection Projection matrix of the camera, with the depth range of the
+         * render system.
+         * @return The value for the depth buffer.
+         */
+        static float TileDepthToBuffer(const float depth, const Ogre::Matrix4& projection);
+
         /**
          * Updates the UV vector of a tile
          *
@@ -765,6 +807,76 @@ class Background2D : public Ogre::RenderQueueListener{
         /**
          * Alpha blending render operation.
          */
+        /**
+         * The number of layers of a background.
+         */
+        static const int LAYER_COUNT = 4;
+
+        /**
+         * Part of a vertex buffer that has the tiles of a layer.
+         */
+        struct LayerRange{
+            LayerRange(): first(0), count(0){}
+            unsigned int first;
+            unsigned int count;
+        };
+
+        /**
+         * The tiles of each layer in the buffers of alpha, add and subtract tiles. They are only
+         * used if the tiles of a layer are together, which is how the installer writes them.
+         */
+        LayerRange layer_ranges_[3][LAYER_COUNT];
+
+        /**
+         * The last layer a tile has been added to for each buffer.
+         */
+        int last_layer_[3];
+
+        /**
+         * If the tiles of the layers of each buffer are together, one layer after the other.
+         */
+        bool layers_contiguous_[3];
+
+        /**
+         * Scroll of each layer relative to the one of the background.
+         */
+        float layer_parallax_[LAYER_COUNT][2];
+
+        /**
+         * Speed at which each layer scrolls by itself, in pixels of the virtual screen per second.
+         */
+        float layer_scroll_speed_[LAYER_COUNT][2];
+
+        /**
+         * Distance each layer has scrolled by itself, in pixels of the virtual screen.
+         */
+        float layer_scroll_offset_[LAYER_COUNT][2];
+
+        /**
+         * Sets the layers to the state they have when a background is loaded.
+         */
+        void ResetLayers();
+
+        /**
+         * If any layer is not scrolled as the background is.
+         */
+        bool HasLayerScroll() const;
+
+        /**
+         * Draws the tiles of a buffer, each layer scrolled as it has to be.
+         *
+         * @param[in] buffer Buffer to draw: 0 for alpha, 1 for add and 2 for subtract.
+         * @param[in,out] render_op What to draw.
+         * @param[in] material Material to draw it with.
+         * @param[in] rs_params Parameters of the render system.
+         * @param[in] width Width of the viewport.
+         * @param[in] height Height of the viewport.
+         */
+        void RenderBuffer(
+          const int buffer, Ogre::RenderOperation& render_op, const Ogre::MaterialPtr& material,
+          Ogre::GpuProgramParametersPtr& rs_params, const float width, const float height
+        );
+
         Ogre::RenderOperation alpha_render_op_;
 
         /**

+ 25 - 3
src/core/DialogsManager.cpp

@@ -101,7 +101,10 @@ void DialogsManager::UpdateDebug(){}
 void DialogsManager::OnResize(){}
 
 void DialogsManager::ClearField(){
-    for (unsigned int i = 0; i < messages_.size(); ++ i) HideMessage(i);
+    for (unsigned int i = 0; i < messages_.size(); ++ i){
+        HideMessage(i);
+        ResetMessageMode(i);
+    }
 }
 
 void DialogsManager::ClearBattle(){
@@ -109,7 +112,10 @@ void DialogsManager::ClearBattle(){
 }
 
 void DialogsManager::ClearWorld(){
-    for (unsigned int i = 0; i < messages_.size(); ++ i) HideMessage(i);
+    for (unsigned int i = 0; i < messages_.size(); ++ i){
+        HideMessage(i);
+        ResetMessageMode(i);
+    }
 }
 
 void DialogsManager::ScriptSetMapName(const char* text_id){
@@ -134,6 +140,11 @@ void DialogsManager::OpenDialog(const char* d_name, int x, int y, int w, int h){
         );
         return;
     }
+    // The mode is for the script that set it. When another one opens the window it is the default.
+    if (!data->mode_owner.empty()){
+        const ScriptId script = ScriptManager::getSingleton().GetCurrentScriptId();
+        if (data->mode_owner != script.entity + "/" + script.function) ResetMessageMode(id);
+    }
     data->x = x;
     data->y = y;
     data->w = w;
@@ -238,6 +249,10 @@ void DialogsManager::SetMode(const char* d_name, const int bg, const bool closea
         return;
     }
     messages_[id]->closeable = closeable;
+    {
+        const ScriptId script = ScriptManager::getSingleton().GetCurrentScriptId();
+        messages_[id]->mode_owner = script.entity + "/" + script.function;
+    }
     switch (bg){
         case 1:
             messages_[id]->visible = false;
@@ -366,7 +381,14 @@ void DialogsManager::HideMessage(const int id){
     for (unsigned int i = 0; i < messages_[id]->sync.size(); ++ i)
         ScriptManager::getSingleton().ContinueScriptExecution(messages_[id]->sync[i]);
     messages_[id]->sync.clear();
-    // Reset mode.
+    // The mode (WMODE) is not reset here: it is kept for the script that set it, so one that is
+    // set once is used by all the messages that script opens, as in the scenes that show several
+    // texts over a black screen, with no window. See OpenDialog. It is also reset when the
+    // field changes.
+}
+
+void DialogsManager::ResetMessageMode(const int id){
+    messages_[id]->mode_owner.clear();
     messages_[id]->closeable = true;
     messages_[id]->visible = true;
     messages_[id]->translucent = false;

+ 14 - 0
src/core/DialogsManager.h

@@ -222,6 +222,13 @@ struct MessageData{
      */
     bool translucent;
 
+    /**
+     * The script that set the mode of the message, as "entity/function", or empty if the mode is
+     * the default one. The mode is used by the messages that script opens, and set back to the
+     * default when another script opens one.
+     */
+    std::string mode_owner;
+
     /**
      * Indicates if the window is for a timer.
      */
@@ -459,6 +466,13 @@ class DialogsManager : public Manager, public Ogre::Singleton<DialogsManager>{
          */
         void HideMessage(const int id);
 
+        /**
+         * Sets the mode of a message (see {@see SetMode}) back to the default one.
+         *
+         * @param[in] id The ID of the message.
+         */
+        void ResetMessageMode(const int id);
+
         /**
          * Retrieves a message Id from it's name.
          *

+ 5 - 0
src/core/Entity.cpp

@@ -243,6 +243,11 @@ void Entity::ScriptSetPosition(const float x, const float y, const float z){
     move_triangle_id_ = -1;
 }
 
+void Entity::ScriptSetPositionXY(const float x, const float y){
+    SetPosition(Ogre::Vector3(x, y, GetPosition().z));
+    move_triangle_id_ = -1;
+}
+
   void Entity::ScriptMoveBy(const float x, const float y, const float z){
     SetPosition(GetPosition() + Ogre::Vector3(x, y, z));
     move_triangle_id_ = -1;

+ 6 - 0
src/core/Entity.h

@@ -225,6 +225,12 @@ class Entity{
          */
         void ScriptSetPosition(const float x, const float y, const float z);
 
+        /**
+         * Sets the X and Y position from a script, keeping Z and the visibility, solidity and
+         * talkability of the entity. The walkmesh triangle is looked up again on the next move.
+         */
+        void ScriptSetPositionXY(const float x, const float y);
+
         /**
          * Moves the entity by the specified position offset.
          *

+ 91 - 26
src/core/EntityManager.cpp

@@ -35,6 +35,17 @@
 /**
  * Entity manager singleton.
  */
+/**
+ * Priority of the script that runs when the player presses OK in front of an entity.
+ *
+ * It has to run before the scripts that fields leave running on an entity (the loops that play an
+ * animation, requested with priority 5 or 6), or the entity could not be talked to. But it can't
+ * be the highest: a script of the same entity that is requested with a higher priority (3 or
+ * smaller) has to be able to run while the talk script waits for it, as in the scenes where a
+ * talk script asks its own entity to do something and waits for it to start.
+ */
+static const int INTERACT_SCRIPT_PRIORITY = 4;
+
 template<>EntityManager *Ogre::Singleton<EntityManager>::msSingleton = nullptr;
 
 ConfigVar cv_debug_grid("debug_grid", "Draw debug grid", "false");
@@ -182,7 +193,7 @@ void EntityManager::Input(const VGears::Event& event){
                       ScriptManager::ENTITY, entity_triggers_[i]->GetName()
                     );
                     bool added = ScriptManager::getSingleton().ScriptRequest(
-                      scr_entity, "on_interact", 1, "", "", false, false
+                      scr_entity, "on_interact", INTERACT_SCRIPT_PRIORITY, "", "", false, false
                     );
                     if (added == false){
                         LOG_WARNING(
@@ -193,10 +204,6 @@ void EntityManager::Input(const VGears::Event& event){
                 }
             }
         }
-        // Rotate move vector to field move rotation
-        Ogre::Quaternion q1;
-        q1.FromAngleAxis(player_move_rotation_, Ogre::Vector3::UNIT_Z);
-        player_move_ = q1 * player_move_;
     }
 }
 
@@ -210,9 +217,36 @@ void EntityManager::UpdateField(){
     if (player_entity_ != nullptr && player_move_ != Ogre::Vector3::ZERO){
         Entity::State state = player_entity_->GetState();
         if (state == Entity::WALKMESH || state == Entity::NONE){
-            player_move_
-              *= player_entity_->GetMoveWalkSpeed() * Timer::getSingleton().GetGameTimeDelta();
-            player_entity_->SetMovePosition(player_entity_->GetPosition() + player_move_);
+            // The directional keys are relative to the screen. Rotate them to the field's move
+            // rotation here, once per frame. This can't be done in Input, which runs once for each
+            // event, so the rotation would be applied several times when there is more than one
+            // event in the frame (diagonals, running, ...). player_move_ stays as the raw input,
+            // which is what ladders need.
+            // A scripted walk of an entity that isn't the player keeps the last triangle when it
+            // goes over an edge, so the stored triangle can be far from the position. Reattach to
+            // the walkmesh before moving manually, otherwise every move would be blocked.
+            const int current_triangle = player_entity_->GetMoveTriangleId();
+            bool in_current_triangle = false;
+            if (current_triangle >= 0 && current_triangle < walkmesh_.GetNumberOfTriangles()){
+                const Ogre::Vector3 position = player_entity_->GetPosition();
+                const Ogre::Vector3 A = walkmesh_.GetA(current_triangle);
+                const Ogre::Vector3 B = walkmesh_.GetB(current_triangle);
+                const Ogre::Vector3 C = walkmesh_.GetC(current_triangle);
+                in_current_triangle = Ogre::Math::pointInTri2D(
+                  Ogre::Vector2(position.x, position.y), Ogre::Vector2(A.x, A.y),
+                  Ogre::Vector2(B.x, B.y), Ogre::Vector2(C.x, C.y)
+                );
+            }
+            if (!in_current_triangle) SetEntityOnWalkmesh(player_entity_);
+
+            Ogre::Quaternion rotation;
+            rotation.FromAngleAxis(player_move_rotation_, Ogre::Vector3::UNIT_Z);
+            // Normalized, so diagonals aren't faster than straight moves.
+            Ogre::Vector3 input = player_move_;
+            input.normalise();
+            const Ogre::Vector3 walk_move = rotation * input
+              * (player_entity_->GetMoveWalkSpeed() * Timer::getSingleton().GetGameTimeDelta());
+            player_entity_->SetMovePosition(player_entity_->GetPosition() + walk_move);
             player_entity_->SetState(Entity::WALKMESH);
         }
 
@@ -740,10 +774,16 @@ void EntityManager::ScriptSetPlayerEntity(const char* name){
     }
     for (unsigned int i = 0; i < entity_.size(); ++ i){
         if (entity_[i]->GetName() == name){
+            // A script can read the walkmesh triangle of the player right after this, so if it was
+            // placed by position the triangle has to be known. Done before it's the player entity
+            // so that the entity not being on the walkmesh yet isn't logged as an error.
+            if (entity_[i]->GetMoveTriangleId() == -1) SetEntityOnWalkmesh(entity_[i]);
+            LOG_TRIVIAL("Player entity set to '" + std::string(name) + "'.");
             player_entity_ = entity_[i];
-            // Also, make is solid, visible, and non-talkable.
-            entity_[i]->SetSolid(true);
-            entity_[i]->SetVisible(true);
+            // Also, make it non-talkable. Visibility and solidity are left to the scripts, as in
+            // the original game: some fields use hidden helper entities (like the 'camera') as
+            // the player entity to scroll the screen, and they must stay invisible and
+            // non-solid. When the player moves manually, UpdateField makes it solid anyway.
             entity_[i]->SetTalkable(false);
         }
     }
@@ -934,10 +974,10 @@ void EntityManager::SetEncounterRate(float rate){
 }
 
 bool EntityManager::IsKeyOn(unsigned int key_code){
-    // If the player is locked, always false.
-    // TODO: Maybe this will interfere with something down the road?
-    // Some scene where the player can't move but must press a button?
-    if (player_lock_) return false;
+    // The keys are read even if the player is locked: scenes lock the player and still ask for a
+    // button, as the minigames that need to be pressed at the right time do. The scripts that
+    // must not read the keys at some point (while a message is open, for example) have their own
+    // flag for it. The triggers that check keys aren't run while the player is locked.
     // The key code of the scripts is a mask of the pad buttons, and the check is true if any of
     // them is pressed. Translate each to its game event (see bind_game_event in config.cfg), so
     // the keys chosen in the controller settings are the ones that count. For example 32 is
@@ -1119,6 +1159,14 @@ bool EntityManager::PerformWalkmeshMove(Entity* entity, const float speed, const
         angle = (direction.x < 0) ? Ogre::Degree(360) - angle : angle;
         entity->SetRotation(Ogre::Degree(angle));
     }
+    // A scripted walk (also of the player entity, while a scene has it locked) must end, or the
+    // scene waiting for it would never go on. Several scripts place solid entities next to where
+    // another one has to walk to, so they don't block it: only the walkmesh does. A manual move
+    // of the player is never scripted.
+    // The same goes for the points around the end of the step: they are there for the player and
+    // NPCs to slide along walls, and a destination next to an edge of the walkmesh would never be
+    // reached. The end point itself is still checked.
+    const bool check_probes = !scripted_move;
     float solid = (entity->IsSolid() == true) ? entity->GetSolidRadius() : 0.01f;
 
     // For not playable entities, be a bit more lenient with solid collisions.
@@ -1150,10 +1198,10 @@ bool EntityManager::PerformWalkmeshMove(Entity* entity, const float speed, const
         end_point2.x = end_point.x + rotation.x;
         end_point2.y = end_point.y + rotation.y;
         // Check_triangle.
-        first_triangle_check = WalkmeshBorderCross(entity, end_point2, direction);
+        first_triangle_check = check_probes && WalkmeshBorderCross(entity, end_point2, direction);
         entity->SetMoveTriangleId(current_triangle);
         // Model check.
-        first_entity_check = CheckSolidCollisions(entity, end_point2);
+        first_entity_check = check_probes && CheckSolidCollisions(entity, end_point2);
 
         // 2nd check.
         // Rotate move_vector +45.
@@ -1168,10 +1216,10 @@ bool EntityManager::PerformWalkmeshMove(Entity* entity, const float speed, const
         end_point2.x = end_point.x + rotation.x;
         end_point2.y = end_point.y + rotation.y;
         // Check triangle.
-        second_triangle_check = WalkmeshBorderCross(entity, end_point2, direction);
+        second_triangle_check = check_probes && WalkmeshBorderCross(entity, end_point2, direction);
         entity->SetMoveTriangleId(current_triangle);
         // Model check.
-        second_entity_check = CheckSolidCollisions(entity, end_point2);
+        second_entity_check = check_probes && CheckSolidCollisions(entity, end_point2);
 
         // 3rd check.
         rotation.x = direction.x;
@@ -1182,10 +1230,10 @@ bool EntityManager::PerformWalkmeshMove(Entity* entity, const float speed, const
         end_point2.x = end_point.x + rotation.x;
         end_point2.y = end_point.y + rotation.y;
         // Check triangle.
-        third_triangle_check = WalkmeshBorderCross(entity, end_point2, direction);
+        third_triangle_check = check_probes && WalkmeshBorderCross(entity, end_point2, direction);
         entity->SetMoveTriangleId(current_triangle);
         // Model check.
-        third_entity_check = CheckSolidCollisions(entity, end_point2);
+        third_entity_check = check_probes && CheckSolidCollisions(entity, end_point2);
 
         // Check condition and modify move_vector.
         if (
@@ -1329,7 +1377,11 @@ bool EntityManager::WalkmeshBorderCross(
     }
     int current_triangle = entity->GetMoveTriangleId();
     if (current_triangle == -1) return true;
-    const bool free_walk = !WalkmeshEdgeBlocks(entity == player_entity_, entity->IsScriptedMove());
+    // While a scene has the player locked, the scene walks the player like any other character,
+    // and it can lead over the edge of the walkmesh.
+    const bool free_walk = !WalkmeshEdgeBlocks(
+      entity == player_entity_ && !player_lock_, entity->IsScriptedMove()
+    );
     Ogre::Vector2 pos = Ogre::Vector2(position.x, position.y);
     for (;;){
         Ogre::Vector3 A3 = walkmesh_.GetA(current_triangle);
@@ -1597,7 +1649,7 @@ void EntityManager::CheckEntityInteract(){
           ScriptManager::ENTITY, name
         );
         bool added = ScriptManager::getSingleton().ScriptRequest(
-          scr_entity, "on_interact", 1, "", "", false, false
+          scr_entity, "on_interact", INTERACT_SCRIPT_PRIORITY, "", "", false, false
         );
         if (added == false){
             LOG_WARNING("Script 'on_interact' for entity '" +  name + "' doesn't exist.");
@@ -1680,7 +1732,15 @@ void EntityManager::SetNextLinearStep(Entity* entity){
         }
     }
     else is_move = true;
-    if (is_move == true){
+    if (is_move == true && (end - start).length() < 0.0001f){
+        // Start and end are the same place, so there is nothing to walk. Dividing by the time to
+        // cross the line (zero) would give a NaN position. Just finish the move.
+        entity->UnsetLinear();
+        if (to_end == true) entity->SetMoveTriangleId(entity->GetLinearDestTriangle());
+        SetEntityOnWalkmesh(entity);
+        entity->SetPosition(to_end ? end : start);
+    }
+    else if (is_move == true){
         Ogre::Vector3 position;
         if (to_end == true){
             Ogre::Vector3 end_start = end - start;
@@ -1695,7 +1755,11 @@ void EntityManager::SetNextLinearStep(Entity* entity){
               ? std::max(position.y, end.y) : std::min(position.y, end.y);
             position.z = (end_start.z < 0)
               ? std::max(position.z, end.z) : std::min(position.z, end.z);
-            if (position == end){
+            // Compared with a tolerance: if the entity starts a bit off the end in one of the
+            // axes (the entry points are written with a few digits only), that axis could move
+            // less than the precision of a float per frame and never be exactly equal.
+            if ((position - end).length() < 0.0001f){
+                position = end;
                 entity->UnsetLinear();
                 entity->SetMoveTriangleId(entity->GetLinearDestTriangle());
                 SetEntityOnWalkmesh(entity);
@@ -1714,7 +1778,8 @@ void EntityManager::SetNextLinearStep(Entity* entity){
               ? std::max(position.y, start.y) : std::min(position.y, start.y);
             position.z = (end_start.z < 0)
               ? std::max(position.z, start.z) : std::min(position.z, start.z);
-            if (position == start){
+            if ((position - start).length() < 0.0001f){
+                position = start;
                 entity->UnsetLinear();
                 SetEntityOnWalkmesh(entity);
             }

+ 52 - 38
src/core/ScriptManager.cpp

@@ -13,6 +13,7 @@
  * GNU General Public License for more details.
  */
 
+#include <algorithm>
 #include "core/ConfigVar.h"
 #include "core/DebugDraw.h"
 #include "core/Logger.h"
@@ -186,23 +187,28 @@ void ScriptManager::Update(const ScriptManager::Type type){
 
     for (unsigned int i = 0; i < script_entity_.size(); ++ i){
         if (script_entity_[i].type == type){
-            if (script_entity_[i].queue.size() > 0){
+            // One script of an entity is run each cycle, the first in the queue. If it yields
+            // without waiting for anything, as the ones that loop for ever, or until a flag
+            // changes, do, it doesn't take the cycle of the rest: the next one is run too. In the
+            // original game the scripts of an entity that have different priorities all run.
+            for (size_t script_index = 0; script_index < script_entity_[i].queue.size(); ++ script_index){
+                bool yielded = false;
                 current_script_id_.entity = script_entity_[i].name;
-                current_script_id_.function = script_entity_[i].queue[0].function;
+                current_script_id_.function = script_entity_[i].queue[script_index].function;
 
-                if (script_entity_[i].queue[0].wait == false){
-                    if (script_entity_[i].queue[0].yield == false){
+                if (script_entity_[i].queue[script_index].wait == false){
+                    if (script_entity_[i].queue[script_index].yield == false){
                         LOG_TRIVIAL(
                           "[SCRIPT] Start script \"" + current_script_id_.function
                           + "\" for entity \"" + current_script_id_.entity + "\"."
                         );
 
-                        for (const ScriptId& waiting : script_entity_[i].queue[0].paused_scripts_start)
+                        for (const ScriptId& waiting : script_entity_[i].queue[script_index].paused_scripts_start)
                             ContinueScriptExecution(waiting);
-                        script_entity_[i].queue[0].paused_scripts_start.clear();
+                        script_entity_[i].queue[script_index].paused_scripts_start.clear();
                         sol::object table = GetTableByEntityName(
                           type, current_script_id_.entity,
-                          script_entity_[i].queue[0].state
+                          script_entity_[i].queue[script_index].state
                         );
                         sol::object function;
                         if (table.get_type() == sol::type::table)
@@ -212,12 +218,12 @@ void ScriptManager::Update(const ScriptManager::Type type){
                           && function.get_type() == sol::type::function
                         ){
                             int ret = 0;
-                            lua_State* thread = script_entity_[i].queue[0].state;
+                            lua_State* thread = script_entity_[i].queue[script_index].state;
                             sol::coroutine coroutine(thread, sol::function(function));
                             auto result = coroutine(
-                              table, script_entity_[i].queue[0].argument1,
-                              script_entity_[i].queue[0].argument2,
-                              script_entity_[i].queue[0].argument3
+                              table, script_entity_[i].queue[script_index].argument1,
+                              script_entity_[i].queue[script_index].argument2,
+                              script_entity_[i].queue[script_index].argument3
                             );
                             if (result.valid()){
                                 // The coroutine yields with the bound function
@@ -231,7 +237,7 @@ void ScriptManager::Update(const ScriptManager::Type type){
                                 sol::error error = result;
                                 std::string msg = "LUA error in entity " + script_entity_[i].name
                                   + " (" + current_script_id_.entity + ") " + " in function "
-                                  + script_entity_[i].queue[0].function + " ("
+                                  + script_entity_[i].queue[script_index].function + " ("
                                   + current_script_id_.function + ")";
                                 if (lua_tostring(thread, -1) != nullptr){
                                     msg = msg + ". Details: " + std::string(
@@ -248,8 +254,8 @@ void ScriptManager::Update(const ScriptManager::Type type){
                                   "[SCRIPT] Script \"" + current_script_id_.function
                                   + "\" for entity \"" + current_script_id_.entity + "\" finished."
                                 );
-                                if (script_entity_[i].queue[0].function != "on_update")
-                                    RemoveEntityTopScript(script_entity_[i]);
+                                if (script_entity_[i].queue[script_index].function != "on_update")
+                                    RemoveEntityScript(script_entity_[i], script_index);
                             }
                             else if (ret == 1){
                                 LOG_TRIVIAL(
@@ -257,7 +263,8 @@ void ScriptManager::Update(const ScriptManager::Type type){
                                   + "\" for entity \"" + current_script_id_.entity
                                   + "\" not paused and will be continued next cycle."
                                 );
-                                script_entity_[i].queue[0].yield = true;
+                                script_entity_[i].queue[script_index].yield = true;
+                                yielded = true;
                             }
                             else{
                                 LOG_TRIVIAL(
@@ -265,8 +272,8 @@ void ScriptManager::Update(const ScriptManager::Type type){
                                   + "\" for entity \"" + current_script_id_.entity
                                   + "\" not finished yet."
                                 );
-                                script_entity_[i].queue[0].yield = true;
-                                script_entity_[i].queue[0].wait = true;
+                                script_entity_[i].queue[script_index].yield = true;
+                                script_entity_[i].queue[script_index].wait = true;
                             }
                         }
                         else{
@@ -274,7 +281,7 @@ void ScriptManager::Update(const ScriptManager::Type type){
                               "Script \"" + current_script_id_.function + "\" for entity \""
                               + current_script_id_.entity + "\" doesn't exist."
                             );
-                            RemoveEntityTopScript(script_entity_[i]);
+                            RemoveEntityScript(script_entity_[i], script_index);
                         }
                     }
                     else{
@@ -284,7 +291,7 @@ void ScriptManager::Update(const ScriptManager::Type type){
                         );
 
                         int ret = 0;
-                        lua_State* thread = script_entity_[i].queue[0].state;
+                        lua_State* thread = script_entity_[i].queue[script_index].state;
                         // sol.hpp bundles a compat53 shim: on LuaJIT this maps
                         // to the native 2-argument lua_resume.
                         int status = lua_resume(thread, nullptr, 0);
@@ -308,10 +315,10 @@ void ScriptManager::Update(const ScriptManager::Type type){
                             );
 
                             // Stop yield for on_update.
-                            script_entity_[i].queue[0].yield = false;
+                            script_entity_[i].queue[script_index].yield = false;
 
-                            if (script_entity_[i].queue[0].function != "on_update")
-                                RemoveEntityTopScript(script_entity_[i]);
+                            if (script_entity_[i].queue[script_index].function != "on_update")
+                                RemoveEntityScript(script_entity_[i], script_index);
                         }
                         else if (ret == 1){
                             LOG_TRIVIAL(
@@ -319,6 +326,7 @@ void ScriptManager::Update(const ScriptManager::Type type){
                               + "\" for entity \"" + current_script_id_.entity
                               + "\" not paused and will be continued next cycle."
                             );
+                            yielded = true;
                         }
                         else{
                             LOG_TRIVIAL(
@@ -326,20 +334,22 @@ void ScriptManager::Update(const ScriptManager::Type type){
                               + "\" for entity \"" + current_script_id_.entity
                               + "\" not finished yet."
                             );
-                            script_entity_[i].queue[0].wait = true;
+                            script_entity_[i].queue[script_index].wait = true;
                         }
                     }
                 }
-                else if (script_entity_[i].queue[0].seconds_to_wait > 0){
-                    script_entity_[i].queue[0].seconds_to_wait
+                else if (script_entity_[i].queue[script_index].seconds_to_wait > 0){
+                    script_entity_[i].queue[script_index].seconds_to_wait
                       -= Timer::getSingleton().GetGameTimeDelta();
-                    script_entity_[i].queue[0].seconds_to_wait
-                      = (script_entity_[i].queue[0].seconds_to_wait < 0)
-                        ? 0 : script_entity_[i].queue[0].seconds_to_wait;
+                    script_entity_[i].queue[script_index].seconds_to_wait
+                      = (script_entity_[i].queue[script_index].seconds_to_wait < 0)
+                        ? 0 : script_entity_[i].queue[script_index].seconds_to_wait;
 
-                    if (script_entity_[i].queue[0].seconds_to_wait == 0)
-                        script_entity_[i].queue[0].wait = false;
+                    if (script_entity_[i].queue[script_index].seconds_to_wait == 0)
+                        script_entity_[i].queue[script_index].wait = false;
                 }
+            
+                if (!yielded) break;
             }
         }
     }
@@ -456,18 +466,20 @@ void ScriptManager::RemoveEntity(const ScriptManager::Type type, const Ogre::Str
     }
 }
 
-void ScriptManager::RemoveEntityTopScript(ScriptEntity& entity){
-    if (entity.queue.size() > 0){
+void ScriptManager::RemoveEntityScript(ScriptEntity& entity, const size_t index){
+    if (entity.queue.size() > index){
         // Delete the thread.
-        luaL_unref(lua_state_, LUA_REGISTRYINDEX, entity.queue[0].state_id);
+        luaL_unref(lua_state_, LUA_REGISTRYINDEX, entity.queue[index].state_id);
 
-        for (const ScriptId& waiting : entity.queue[0].paused_scripts_end)
+        for (const ScriptId& waiting : entity.queue[index].paused_scripts_end)
             ContinueScriptExecution(waiting);
-        entity.queue[0].paused_scripts_end.clear();
-        entity.queue.erase(entity.queue.begin());
+        entity.queue[index].paused_scripts_end.clear();
+        entity.queue.erase(entity.queue.begin() + index);
     }
 }
 
+void ScriptManager::RemoveEntityTopScript(ScriptEntity& entity){RemoveEntityScript(entity, 0);}
+
 sol::object ScriptManager::GetTableByEntityName(
   const ScriptManager::Type type, const Ogre::String& name, lua_State* state
 ) const{
@@ -524,9 +536,11 @@ const ScriptId ScriptManager::GetCurrentScriptId() const{return current_script_i
 void ScriptManager::ContinueScriptExecution(const ScriptId& script){
     QueueScript* script_pointer = GetScriptByScriptId(script);
     if (script_pointer == nullptr){
-        LOG_ERROR(
+        // The script that waited for this is already gone, for example because the field changed
+        // or the script was cancelled, so there is nothing to continue.
+        LOG_TRIVIAL(
           "There is no script \"" + script.function + "\" for entity \""
-          + script.entity + "\". Can't continue script execution."
+          + script.entity + "\" to continue. It has already ended."
         );
         return;
     }

+ 8 - 0
src/core/ScriptManager.h

@@ -295,6 +295,14 @@ class ScriptManager : public Manager, public Ogre::Singleton<ScriptManager>{
           const Ogre::String& entity_name, const Ogre::String& function_name, int priority
         );
 
+        /**
+         * Removes a script of an entity.
+         *
+         * @param[in] entity Entity whose script to remove.
+         * @param[in] index Position of the script in the queue of the entity.
+         */
+        void RemoveEntityScript(ScriptEntity& entity, const size_t index);
+
         /**
          * Removes the top script of an entity.
          *

+ 3 - 0
src/core/ScriptManagerBinds.h

@@ -116,6 +116,7 @@ void ScriptManager::InitBinds(){
     lua.new_usertype<Entity>(
       "Entity",
       "set_position", &Entity::ScriptSetPosition,
+      "set_position_xy", &Entity::ScriptSetPositionXY,
       "move_by", &Entity::ScriptMoveBy,
       "get_position", &Entity::ScriptGetPosition,
       "get_name", &Entity::GetName,
@@ -466,6 +467,8 @@ void ScriptManager::InitBinds(){
       "area_off", &Background2D::ScriptAreaOff,
       "area_clear", &Background2D::ScriptAreaClear,
       "shake", &Background2D::ScriptShake,
+      "set_layer_parallax", &Background2D::ScriptSetLayerParallax,
+      "set_layer_scroll", &Background2D::ScriptSetLayerScroll,
       "play_animation_looped", &Background2D::ScriptPlayAnimationLooped,
       "play_animation_once", &Background2D::ScriptPlayAnimationOnce
     );

+ 6 - 0
src/core/UiManager.cpp

@@ -150,6 +150,12 @@ void UiManager::ApplyWindowColours(UiWidget* widget){
 
 void UiManager::renderQueueStarted(
   Ogre::uint8 queueGroupId, const Ogre::String& invocation, bool& skipThisInvocation
+){
+    // The widgets are drawn when the queue ends. See renderQueueEnded.
+}
+
+void UiManager::renderQueueEnded(
+  Ogre::uint8 queueGroupId, const Ogre::String& invocation, bool& repeatThisInvocation
 ){
     if (queueGroupId == Ogre::RENDER_QUEUE_OVERLAY){
         Ogre::Root::getSingletonPtr()->getRenderSystem()->clearFrameBuffer(Ogre::FBT_DEPTH);

+ 17 - 0
src/core/UiManager.h

@@ -185,6 +185,23 @@ class UiManager :
           bool& skipThisInvocation
         );
 
+        /**
+         * Draws the widgets when the overlay queue ends.
+         *
+         * The screen fade and the movies are drawn when the overlay queue starts, and the order
+         * the listeners are called in is the order they were added in. The UI was added first,
+         * so it was drawn before them, and they were drawn over the dialogs. At the end of the
+         * queue it is drawn over all of them.
+         *
+         * @param[in] queueGroupId The queue group ID.
+         * @param[in] invocation Unused.
+         * @param[in] repeatThisInvocation Unused.
+         */
+        void renderQueueEnded(
+          Ogre::uint8 queueGroupId, const Ogre::String& invocation,
+          bool& repeatThisInvocation
+        );
+
     private:
 
         /**

+ 7 - 6
src/core/XmlBackground2DFile.cpp

@@ -63,18 +63,19 @@ void XmlBackground2DFile::Load(){
                 if (blending_str == "add") blending = VGears::B_ADD;
                 else if (blending_str == "subtract") blending = VGears::B_SUBTRACT;
                 else blending = VGears::B_ALPHA;
-                Ogre::Vector4 distance = Ogre::Vector4(0, 0, -depth, 1);
-                Ogre::Vector4 res
-                  = CameraManager::getSingleton().GetCurrentCamera()
-                    ->getProjectionMatrixWithRSDepth() * distance;
-                res = res / res.w;
+                const float buffer_depth = Background2D::TileDepthToBuffer(
+                  depth,
+                  CameraManager::getSingleton().GetCurrentCamera()->getProjectionMatrixWithRSDepth()
+                );
                 // Tiles that belong to a scripted area are only drawn while a script turns on
                 // the state they belong to.
                 const int area = GetInt(node, "area", 0);
                 const int state = GetInt(node, "state", 0);
+                // The layer of the original background the tile is from.
+                const int layer = GetInt(node, "layer", 0);
                 background->AddTile(
                   static_cast<int>(destination.x), static_cast<int>(destination.y),
-                  width, height, res.z, uv.x, uv.y, uv.z, uv.w, blending, area, state
+                  width, height, buffer_depth, uv.x, uv.y, uv.z, uv.w, blending, area, state, layer
                 );
                 TiXmlNode* node2 = node->FirstChild();
                 while (node2 != nullptr){

+ 5 - 6
src/installer/CMakeLists.txt

@@ -169,9 +169,8 @@ if(OGRE_LIBRARIES)
 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}
+)

+ 5 - 1
src/installer/FieldDataInstaller.cpp

@@ -692,7 +692,7 @@ void FieldDataInstaller::PcFieldToVGearsField(VGears::FLevelFilePtr& field){
                     first_entity_point = position;
                 }
                 xml_entity_point->SetAttribute("position", Ogre::StringConverter::toString(position));
-                const float rotation = (360.0f * static_cast<float>(gateway.dir)) / 255.0f;
+                const float rotation = (360.0f * static_cast<float>(gateway.dir)) / 256.0f;
                 xml_entity_point->SetAttribute("rotation", std::to_string(rotation));
                 element->LinkEndChild(xml_entity_point.release());
             }
@@ -847,7 +847,9 @@ void FieldDataInstaller::PcFieldToVGearsField(VGears::FLevelFilePtr& field){
             );
             // Write out the *_BG.XML data.
             auto& layers = bg->GetLayers();
+            int layer_index = -1;
             for (const auto& layer : layers){
+                ++ layer_index;
                 // TODO: Should only the tiles to construct enabled layers be outputted?
                 //  if (layer.enabled){
 
@@ -889,6 +891,8 @@ void FieldDataInstaller::PcFieldToVGearsField(VGears::FLevelFilePtr& field){
                     // TODO: Should probably throw to fail conversion:
                     else blending_str = "unknown";
                     xml_element->SetAttribute("blending", blending_str);
+                    // The layer is needed by the scripts that scroll them at their own speed.
+                    xml_element->SetAttribute("layer", layer_index);
                     // Sprites in an area the script controls (BGON, BGOFF, BGCLR) are drawn only
                     // while it turns on one of their layers. The rest are always drawn.
                     if (

+ 12 - 2
src/installer/FieldTextWriter.cpp

@@ -65,7 +65,17 @@ void FieldTextWriter::Write(const std::vector<u8>& script_section_buffer, std::s
         logger_->Log("[ERROR exception]");
         logger_->Log("</dialog>\n");
     }
-    const u16 dialogCount = GetU16LE(offset_to_sector + offset_to_dialogs);
+    u16 dialogCount = GetU16LE(offset_to_sector + offset_to_dialogs);
+    // The first string comes right after the table of offsets, so its offset says how many strings
+    // there are. A field with 256 of them has a count that doesn't fit (it is read as 0), and the
+    // field would have no texts at all.
+    if (offset_to_sector + offset_to_dialogs + 0x03 < data_.size()){
+        const u16 first_offset = GetU16LE(offset_to_sector + offset_to_dialogs + 0x02);
+        if (first_offset >= 0x02 && (first_offset % 2) == 0){
+            const u16 count_from_table = (first_offset - 0x02) / 2;
+            if (count_from_table > dialogCount) dialogCount = count_from_table;
+        }
+    }
     for (u16 i = 0; i <dialogCount; ++ i){
         // gGt offset of string data.
         u32 offset = offset_to_sector + offset_to_dialogs + GetU16LE(
@@ -271,7 +281,7 @@ void FieldTextWriter::Write(const std::vector<u8>& script_section_buffer, std::s
                 }
                 else if (temp2 == 0xDC){
                     AddText(dialog);
-                    logger_->Log("<pause_ok >");
+                    logger_->Log("<pause_ok />");
                 }
                 else if (temp2 == 0xDD){
                     u16 wait = GetU16LE(offset + 1);

+ 33 - 76
src/installer/MainWindow.cpp

@@ -37,6 +37,9 @@
 #include <QtCore/QFileInfo>
 #include <QtCore/QStorageInfo>
 #include <chrono>
+#include <OgrePrerequisites.h>
+#include "common/DataDirectory.h"
+#include "common/OgrePlugins.h"
 #include "DataInstaller.h"
 #include "ExternalTools.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.
  */
 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){
+    namespace bfs = boost::filesystem;
     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;
 }
@@ -500,7 +464,8 @@ void MainWindow::LaunchGame(const QString& dir){
     settings_->setValue("VGearsEXE", exe);
     // The game runs from the installation directory, where it finds its data.
     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);
     if (!QProcess::startDetached(exe, args, dir)){
         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;
             }
-            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."));
         }
-        // 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")){
             QMessageBox::critical(
               this, tr("Missing configuration files"),

+ 1 - 1
src/installer/WorldInstaller.cpp

@@ -899,7 +899,7 @@ bool WorldInstaller::GenerateScripts(
                         const int16 triangle = static_cast<int16>(raw_triangle);
                         const uint8 direction = field_table_data[offset + 8];
                         const float direction_degrees
-                            = 360.0f * static_cast<float>(direction) / 255.0f;
+                            = 360.0f * static_cast<float>(direction) / 256.0f;
                         const size_t table_id = record_index + 1;
                         script_file << "System.WorldMap.field_table[" << table_id << "]"
                             << " = System.WorldMap.field_table[" << table_id << "] or {}\n"

+ 79 - 4
src/installer/decompiler/field/FieldCodeGenerator.cpp

@@ -14,7 +14,9 @@
  */
 
 #include <boost/algorithm/string/predicate.hpp>
+#include <algorithm>
 #include <stdexcept>
+#include <unordered_set>
 #include "FieldCodeGenerator.h"
 #include "FieldDisassembler.h"
 #include "FieldEngine.h"
@@ -113,6 +115,7 @@ void FieldCodeGenerator::Generate(InstVec& insts, const Graph& graph){
 
     for (auto function = functions_with_bodies.begin(); function != functions_with_bodies.end(); ++ function){
 
+        const size_t first_line_of_function = lines_.size();
         OnBeforeStartFunction(function->first);
         auto signature = ConstructFuncSignature(function->first);
         AddOutputLine(signature, false, true);
@@ -129,6 +132,31 @@ void FieldCodeGenerator::Generate(InstVec& insts, const Graph& graph){
             }
         }
 
+        // Lua can't jump into a block. If a jump from outside of the body of a conditional lands
+        // in the middle of it, the conditional is written as a jump over its body instead of a
+        // block, so the label ends up where every jump can see it.
+        std::unordered_set<const Instruction*> flattened;
+        for (auto cond = function->second.begin(); cond != function->second.end(); ++ cond){
+            if (!(*cond)->IsCondJump()) continue;
+            const uint32 start = (*cond)->GetAddress();
+            const uint32 end = (*cond)->GetDestAddress();
+            // The target has to be in the function to have a label.
+            const bool target_in_function = std::any_of(
+              function->second.begin(), function->second.end(),
+              [end](const InstPtr& i){return i->GetAddress() == end;}
+            );
+            if (!target_in_function) continue;
+            for (auto jump = function->second.begin(); jump != function->second.end(); ++ jump){
+                if (!(*jump)->IsUncondJump()) continue;
+                const uint32 destination = (*jump)->GetDestAddress();
+                const uint32 origin = (*jump)->GetAddress();
+                if (destination > start && destination < end && (origin < start || origin >= end)){
+                    flattened.insert(cond->get());
+                    break;
+                }
+            }
+        }
+
         // Implemented instructions.
 
         // Some conditional jumps point to an address outside the function (i.e the starting
@@ -141,7 +169,7 @@ void FieldCodeGenerator::Generate(InstVec& insts, const Graph& graph){
                 bool needs_label = false;
                 bool needs_new_line = false;
                 for (auto origin = label->second.begin(); origin != label->second.end(); ++ origin){
-                    if ((*origin)->IsCondJump()){
+                    if ((*origin)->IsCondJump() && flattened.count(origin->get()) == 0){
                         ends_needed --;
                         AddOutputLine("end", true, false);
                         needs_new_line = true;
@@ -157,7 +185,13 @@ void FieldCodeGenerator::Generate(InstVec& insts, const Graph& graph){
             //    AddOutputLine("end -- end if", true, false);
             //    end_needed = false;
             //}
-            if ((*instruction)->IsCondJump()){
+            if ((*instruction)->IsCondJump() && flattened.count(instruction->get()) != 0){
+                AddOutputLine((
+                  boost::format("if not (%s) then goto label_0x%X end")
+                    % stack.Pop()->GetString() % (*instruction)->GetDestAddress()
+                ).str());
+            }
+            else if ((*instruction)->IsCondJump()){
                 ends_needed ++;
                 AddOutputLine((boost::format("if (%s) then") % stack.Pop()->GetString()).str(), false, true);
                 // If the next instruction is the last in the function, mark the next pass to
@@ -192,6 +226,20 @@ void FieldCodeGenerator::Generate(InstVec& insts, const Graph& graph){
             if ("on_update" == function->first.name) AddOutputLine(ON_UPDATE_FINISHED_STATEMENT);
             AddOutputLine("do return 0 end", false, false);
         }
+        // A goto to a label that isn't in the function can't be compiled, and the whole script
+        // would fail to load. It can't continue where it was told to, so it ends the script.
+        {
+            std::unordered_set<std::string> emitted_labels;
+            for (size_t i = first_line_of_function; i < lines_.size(); ++ i)
+                if (lines_[i].line.rfind("::label_0x", 0) == 0) emitted_labels.insert(lines_[i].line);
+            for (size_t i = first_line_of_function; i < lines_.size(); ++ i){
+                const std::string prefix = "goto label_0x";
+                if (lines_[i].line.rfind(prefix, 0) != 0) continue;
+                const std::string label = "::" + lines_[i].line.substr(5) + "::";
+                if (emitted_labels.count(label) == 0)
+                    lines_[i].line = "do return 0 end -- was: " + lines_[i].line + " (not in this function)";
+            }
+        }
         OnEndFunction(function->first);
     }
 
@@ -224,7 +272,7 @@ void FieldCodeGenerator::OnStartFunction(const Function& func){
     if (meta_data.GetEntityName() == "Director" && func.name == "on_start"){
         // TODO: Don't hardcode Cloud, Use the one from the previous level,
         AddOutputLine("-- HACK: Set default playable entity an ensure the camera follows it.");
-        AddOutputLine("entity_manager:set_player_entity(\"Cloud\")");
+        AddOutputLine("place_player_at_map_entry(\"Cloud\")");
         AddOutputLine("background2d:autoscroll_to_entity(entity_manager:get_entity(\"Cloud\"))\n");
     }
     // Don't run on_update again once it has reached its end.
@@ -255,7 +303,34 @@ void FieldCodeGenerator::OnEndFunction(const Function& function){
     AddOutputLine("end,", true, false);
     // End class?
     FunctionMetaData meta_data(function.metadata);
-    if (meta_data.IsEnd()) AddOutputLine("}\n\n\n", true, false);
+    if (meta_data.IsEnd()){
+        AddOutputLine("}\n", true, false);
+        // The scripts of the entity that have the same code as other of its scripts.
+        for (const auto& alias : static_cast<FieldEngine*>(engine_)->GetFunctionAliases()){
+            if (alias.entity_name != meta_data.GetEntityName()) continue;
+            const std::string container = "EntityContainer[\"" + alias.entity_name + "\"]";
+            const std::string alias_name
+              = formatter_.GetFriendlyFunctionName(alias.entity_name, alias.alias);
+            const std::string target_name
+              = formatter_.GetFriendlyFunctionName(alias.entity_name, alias.target);
+            // The events of a line don't run while it is off, as in the functions of their own.
+            if (
+              alias.alias == "on_approach" || alias.alias == "on_cross" || alias.alias == "on_near"
+              || alias.alias == "on_near_once" || alias.alias == "on_leave"
+            ){
+                AddOutputLine(
+                  container + "." + alias_name + " = function(self, ...)", false, true
+                );
+                AddOutputLine("if self.on ~= nil and self.on == false then", false, true);
+                AddOutputLine("return 0");
+                AddOutputLine("end", true, false);
+                AddOutputLine("return " + container + "." + target_name + "(self, ...)");
+                AddOutputLine("end", true, false);
+            }
+            else AddOutputLine(container + "." + alias_name + " = " + container + "." + target_name);
+        }
+        AddOutputLine("\n\n");
+    }
     else AddOutputLine("\n");
 }
 

+ 52 - 2
src/installer/decompiler/field/FieldDisassembler.cpp

@@ -128,6 +128,12 @@ void FieldDisassembler::DoDisassemble(){
         }
         // Only parse each script one.
         std::set<uint16> parsed_scripts;
+        // The first script that has each entry point, and the scripts that repeat it.
+        std::map<uint16, size_t> first_script_with_entry_point;
+        std::vector<std::pair<size_t, size_t>> repeated_scripts;
+        // The unused scripts at the end of an entity point to its last script, as the ones that
+        // are empty point to the one after them. Those are not names for it.
+        const uint16 last_script_entry_point = header_.entity_scripts[entity_number].back();
         std::vector<ScriptInfo> script_info;
         // Collect the scripts to parse
         for (
@@ -136,12 +142,27 @@ void FieldDisassembler::DoDisassemble(){
             uint16 script_entry_point = header_.entity_scripts[entity_number][script_index];
             // If this scripts entry point is already parsed, don't do it again as it means
             // two scripts have the same entry, which only seems to be true for "empty" scripts.
-            if (parsed_scripts.find(script_entry_point) != std::end(parsed_scripts)) continue;
-            parsed_scripts.insert(script_entry_point);
             const uint32 next_script_entry_point = GetEndOfScriptOffset(
               script_entry_point, entity_number, script_index
             );
             const uint32 script_size = next_script_entry_point - script_entry_point;
+            if (parsed_scripts.find(script_entry_point) != std::end(parsed_scripts)){
+                // It is the same code as another script. If it is more than a return it has to
+                // be a name for it, or the event that starts it would be lost. For example, a
+                // line that starts a scene when it is crossed does it with the same code as when
+                // OK is pressed.
+                const size_t first = first_script_with_entry_point[script_entry_point];
+                if (script_size > 1 && first > 0 && script_index > 0){
+                    const bool unused_tail = script_entry_point == last_script_entry_point;
+                    // Only the events of a line can be one of the unused ones: they have a name
+                    // (they are checked below, when it is known that the entity is a line).
+                    if (!unused_tail || script_index <= 6)
+                        repeated_scripts.push_back(std::make_pair(script_index, first));
+                }
+                continue;
+            }
+            parsed_scripts.insert(script_entry_point);
+            first_script_with_entry_point[script_entry_point] = script_index;
             if (script_size > 0){
                 ScriptInfo info = {script_entry_point, next_script_entry_point, script_index};
                 script_info.push_back(info);
@@ -154,6 +175,35 @@ void FieldDisassembler::DoDisassemble(){
               entity_name, entity_number, it->index, it->entry_point, it->next_entry_point, is_start, is_end
             );
         }
+        // Name the repeated scripts, now that it is known if the entity is a line.
+        for (const auto& repeated : repeated_scripts){
+            const bool is_line = engine_->EntityIsLine(entity_number);
+            // The slots of an entity that is not a line don't have a name, only the ones of a
+            // line do. The ones in the unused tail are kept only if they have one.
+            if (
+              header_.entity_scripts[entity_number][repeated.first]
+                == header_.entity_scripts[entity_number].back()
+              && !(is_line && repeated.first <= 6)
+            ) continue;
+            std::string alias = "script_" + std::to_string(repeated.first);
+            if (repeated.first == 1) alias = "on_interact";
+            else if (is_line){
+                switch (repeated.first){
+                    case 2: alias = "on_approach"; break;
+                    case 3: alias = "on_cross"; break;
+                    case 4: alias = "on_near"; break;
+                    case 5: alias = "on_near_once"; break;
+                    case 6: alias = "on_leave"; break;
+                    default: break;
+                }
+            }
+            std::string target;
+            try{target = engine_->EntityByIndex(entity_number).FunctionByIndex(repeated.second);}
+            catch (const DecompilerException&){continue;}
+            if (alias == target) continue;
+            engine_->AddEntityFunction(entity_name, entity_number, alias, repeated.first);
+            engine_->AddFunctionAlias(entity_name, alias, target);
+        }
     }
 }
 

+ 6 - 0
src/installer/decompiler/field/FieldEngine.cpp

@@ -213,6 +213,12 @@ void FieldEngine::AddEntityFunction(
     }
 }
 
+void FieldEngine::AddFunctionAlias(
+  const std::string& entity_name, const std::string& alias, const std::string& target
+){
+    function_aliases_.push_back(FunctionAlias{entity_name, alias, target});
+}
+
 void FieldEngine::MarkEntityAsLine(
   size_t entity_index, bool line, std::vector<float> point_a, std::vector<float> point_b
 ){

+ 35 - 0
src/installer/decompiler/field/FieldEngine.h

@@ -310,6 +310,36 @@ class FieldEngine : public Engine{
           const std::string& func_name, size_t func_index
         );
 
+        /**
+         * A function of an entity that is the same code as another function of it.
+         *
+         * Several scripts of an entity can start at the same address in the original game, so the
+         * code is only written once, and the rest of them are names for it.
+         */
+        struct FunctionAlias{
+            std::string entity_name;
+            std::string alias;
+            std::string target;
+        };
+
+        /**
+         * Adds a function that is another name for a function of the same entity.
+         *
+         * @param[in] entity_name Name of the entity.
+         * @param[in] alias Name of the new function.
+         * @param[in] target Name of the function that has the code.
+         */
+        void AddFunctionAlias(
+          const std::string& entity_name, const std::string& alias, const std::string& target
+        );
+
+        /**
+         * Retrieves the functions that are another name for other function.
+         *
+         * @return The aliases.
+         */
+        const std::vector<FunctionAlias>& GetFunctionAliases() const{return function_aliases_;}
+
         /**
          * Marks an entity as a line.
          *
@@ -403,6 +433,11 @@ class FieldEngine : public Engine{
          */
         std::map<size_t, Entity> entity_index_map_;
 
+        /**
+         * Functions that are another name for other functions.
+         */
+        std::vector<FunctionAlias> function_aliases_;
+
         /**
          * The map scale factor.
          */

+ 38 - 2
src/installer/decompiler/field/instruction/FieldBackgroundInstruction.cpp

@@ -16,6 +16,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include <cstdint>
 #include <iostream>
 #include <sstream>
 #include <boost/format.hpp>
@@ -30,8 +31,8 @@ void FieldBackgroundInstruction::ProcessInst(
     FunctionMetaData md(func.metadata);
     //std::cout << "Background opcode: " << opcode_ << "\n";
     switch (opcode_){
-        case OPCODES::BGPDH: code_gen->WriteTodo(md.GetEntityName(), "BGPDH");break;
-        case OPCODES::BGSCR: code_gen->WriteTodo(md.GetEntityName(), "BGSCR"); break;
+        case OPCODES::BGPDH: ProcessBGPDH(code_gen); break;
+        case OPCODES::BGSCR: ProcessBGSCR(code_gen); break;
         case OPCODES::MPPAL: code_gen->WriteTodo(md.GetEntityName(), "MPPAL"); break;
         case OPCODES::BGON: ProcessBGON(code_gen, static_cast<FieldEngine&>(*engine)); break;
         case OPCODES::BGOFF: ProcessBGOFF(code_gen, static_cast<FieldEngine&>(*engine)); break;
@@ -56,6 +57,41 @@ void FieldBackgroundInstruction::ProcessInst(
     }
 }
 
+void FieldBackgroundInstruction::ProcessBGPDH(CodeGenerator* code_gen){
+    // Parallax distance of a background layer. The layer is a value, and the distance can be a
+    // value or a variable (the bank is the first nibble).
+    FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
+    const auto& distance = FieldCodeGenerator::FormatValueOrVariable(
+      cg->GetFormatter(), params_[0]->GetUnsigned(), static_cast<int>(
+        static_cast<int16_t>(params_[3]->GetUnsigned() & 0xFFFF)
+      )
+    );
+    code_gen->AddOutputLine((
+      boost::format("background2d:set_layer_parallax(%1%, %2%)") % params_[2]->GetUnsigned()
+        % distance
+    ).str());
+}
+
+void FieldBackgroundInstruction::ProcessBGSCR(CodeGenerator* code_gen){
+    // Speed at which a background layer scrolls by itself, on each axis. The layer is a value. X
+    // and Y can be values or variables, with the bank in the first and second nibbles.
+    FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
+    const auto& x = FieldCodeGenerator::FormatValueOrVariable(
+      cg->GetFormatter(), params_[0]->GetUnsigned(), static_cast<int>(
+        static_cast<int16_t>(params_[3]->GetUnsigned() & 0xFFFF)
+      )
+    );
+    const auto& y = FieldCodeGenerator::FormatValueOrVariable(
+      cg->GetFormatter(), params_[1]->GetUnsigned(), static_cast<int>(
+        static_cast<int16_t>(params_[4]->GetUnsigned() & 0xFFFF)
+      )
+    );
+    code_gen->AddOutputLine((
+      boost::format("background2d:set_layer_scroll(%1%, %2%, %3%)") % params_[2]->GetUnsigned()
+        % x % y
+    ).str());
+}
+
 void FieldBackgroundInstruction::ProcessBGON(CodeGenerator* code_gen, FieldEngine& engine){
     engine.AddBackgroundArea(params_[0]->GetUnsigned() == 0 ? params_[2]->GetUnsigned() : -1);
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);

+ 4 - 0
src/installer/decompiler/field/instruction/FieldBackgroundInstruction.h

@@ -41,6 +41,10 @@ class FieldBackgroundInstruction : public KernelCallInstruction{
 
     private:
 
+        void ProcessBGPDH(CodeGenerator* code_gen);
+
+        void ProcessBGSCR(CodeGenerator* code_gen);
+
         void ProcessBGON(CodeGenerator* code_gen, FieldEngine& engine);
         void ProcessBGOFF(CodeGenerator* code_gen, FieldEngine& engine);
         void ProcessBGCLR(CodeGenerator* code_gen, FieldEngine& engine);

+ 38 - 12
src/installer/decompiler/field/instruction/FieldCondJumpInstruction.cpp

@@ -16,6 +16,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include <cstdint>
 #include <iostream>
 #include <sstream>
 #include <boost/format.hpp>
@@ -46,8 +47,10 @@ void FieldCondJumpInstruction::ProcessInst(
           || opcode_ == OPCODES::IFKEYON
           || opcode_ == OPCODES::IFKEYOFF
         ){
-            if (param >= 1024) param = param ^ 1024;
-            if (param >= 512) param = param ^ 512;
+            // XOR only if the bit is set, to clear it. Testing with >= added them to the keys
+            // that had a bigger value, such as 32768.
+            if (param & 1024) param = param ^ 1024;
+            if (param & 512) param = param ^ 512;
         }
         ValuePtr v = new UnquotedStringValue(func_name + "(" + std::to_string(param) + ")");
         stack.Push(v);
@@ -55,12 +58,25 @@ void FieldCondJumpInstruction::ProcessInst(
     }
     std::string op;
     uint32 type = params_[4]->GetUnsigned();
-    const auto& source = FieldCodeGenerator::FormatValueOrVariable(
-      cg->GetFormatter(), params_[0]->GetUnsigned(), params_[2]->GetUnsigned()
-    );
-    const auto& destination = FieldCodeGenerator::FormatValueOrVariable(
-      cg->GetFormatter(), params_[1]->GetUnsigned(), params_[3]->GetUnsigned()
-    );
+    // The values of the signed comparisons (IFSW) are signed words: 65504 is -32. The ones of the
+    // unsigned comparisons, and the addresses of the variables, are not.
+    const bool is_signed = opcode_ == OPCODES::IFSW || opcode_ == OPCODES::IFSWL;
+    const auto& source = is_signed
+      ? FieldCodeGenerator::FormatValueOrVariable(
+          cg->GetFormatter(), params_[0]->GetUnsigned(),
+          static_cast<int>(static_cast<int16_t>(params_[2]->GetUnsigned() & 0xFFFF))
+        )
+      : FieldCodeGenerator::FormatValueOrVariable(
+          cg->GetFormatter(), params_[0]->GetUnsigned(), params_[2]->GetUnsigned()
+        );
+    const auto& destination = is_signed
+      ? FieldCodeGenerator::FormatValueOrVariable(
+          cg->GetFormatter(), params_[1]->GetUnsigned(),
+          static_cast<int>(static_cast<int16_t>(params_[3]->GetUnsigned() & 0xFFFF))
+        )
+      : FieldCodeGenerator::FormatValueOrVariable(
+          cg->GetFormatter(), params_[1]->GetUnsigned(), params_[3]->GetUnsigned()
+        );
 
     switch (type){
         case 0: op = "=="; break;
@@ -69,10 +85,20 @@ void FieldCondJumpInstruction::ProcessInst(
         case 3: op = "<"; break;
         case 4: op = ">="; break;
         case 5: op = "<="; break;
-        // TODO: FIX those, use the new binary functions.
-        case 6: op = "&"; break;
-        case 7: op = "^"; break;
-        case 8: op = "|"; break;
+        // The game runs on LuaJIT (Lua 5.1), where there are no operators for the bits, and a
+        // number is always true, even if it is 0. These are true if the result isn't zero.
+        case 6:
+        case 7:
+        case 8:
+            {
+                const std::string function
+                  = (type == 6) ? "bit_and" : ((type == 7) ? "bit_xor" : "bit_or");
+                ValuePtr v = new UnquotedStringValue(
+                  function + "(" + source + ", " + destination + ") ~= 0"
+                );
+                stack.Push(v);
+            }
+            return;
         case 9:
             {
                 op = "get_bank_bit(" + params_[0]->GetString() + ", " + params_[2]->GetString()

+ 45 - 4
src/installer/decompiler/field/instruction/FieldModelInstruction.cpp

@@ -101,7 +101,11 @@ void FieldModelInstruction::ProcessInst(
         case OPCODES::PGTDR: code_gen->WriteTodo(md.GetEntityName(), "PGTDR"); break;
         case OPCODES::PXYZI: code_gen->WriteTodo(md.GetEntityName(), "PXYZI"); break;
         case OPCODES::TLKON: ProcessTLKON(code_gen, md.GetEntityName()); break;
-        case OPCODES::PC: ProcessPC(code_gen, md.GetEntityName(), md.GetCharacterId()); break;
+        case OPCODES::PC:
+            // The character is the one in the opcode. The one of CHAR is the model of the field's
+            // list, and it is not the same for all (for Aeris it is 1, and she is character 3).
+            ProcessPC(code_gen, md.GetEntityName(), params_[0]->GetUnsigned());
+            break;
         case OPCODES::opCodeCHAR: ProcessCHAR(code_gen, md.GetEntityName()); break;
         case OPCODES::DFANM: ProcessDFANM(code_gen, md.GetEntityName(), md.GetCharacterId()); break;
         case OPCODES::ANIME1:
@@ -109,7 +113,7 @@ void FieldModelInstruction::ProcessInst(
             break;
         case OPCODES::VISI: ProcessVISI(code_gen, md.GetEntityName()); break;
         case OPCODES::XYZI: ProcessXYZI(code_gen, md.GetEntityName()); break;
-        case OPCODES::XYI: code_gen->WriteTodo(md.GetEntityName(), "XYI"); break;
+        case OPCODES::XYI: ProcessXYI(code_gen, md.GetEntityName()); break;
         case OPCODES::XYZ: code_gen->WriteTodo(md.GetEntityName(), "XYZ"); break;
         case OPCODES::MOVE: ProcessMOVE(code_gen, md.GetEntityName()); break;
         case OPCODES::CMOVE: code_gen->WriteTodo(md.GetEntityName(), "CMOVE"); break;
@@ -135,7 +139,7 @@ void FieldModelInstruction::ProcessInst(
         case OPCODES::TURN: code_gen->WriteTodo(md.GetEntityName(), "TURN"); break;
         case OPCODES::DIRA: ProcessDIRA(code_gen, md.GetEntityName(), eng); break;
         case OPCODES::GETDIR: code_gen->WriteTodo(md.GetEntityName(), "GETDIR"); break;
-        case OPCODES::GETAXY: code_gen->WriteTodo(md.GetEntityName(), "GETAXY"); break;
+        case OPCODES::GETAXY: ProcessGETAXY(code_gen); break;
         case OPCODES::GETAI: ProcessGETAI(code_gen, eng); break;
         case OPCODES::ANIM_2:
             ProcessANIM_2(code_gen, md.GetEntityName(), md.GetCharacterId());
@@ -455,6 +459,29 @@ void FieldModelInstruction::ProcessXYZI(CodeGenerator* code_gen, const std::stri
     ).str());
 }
 
+void FieldModelInstruction::ProcessXYI(CodeGenerator* code_gen, const std::string& entity){
+    FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
+    const float scale = 128.0f * cg->GetScaleFactor();
+    // Constants are already scaled by the formatter, but values read from a bank (usually stored
+    // by AXYZI) are in field units, so they need to be scaled here.
+    const auto format_coordinate = [&](uint32 bank, int16 value){
+        const auto& formatted = FieldCodeGenerator::FormatValueOrVariable(
+          cg->GetFormatter(), bank, value, FieldCodeGenerator::ValueType::Float, scale
+        );
+        if (bank == 0) return formatted;
+        return "(" + formatted + ") / " + std::to_string(scale);
+    };
+    const auto x = format_coordinate(params_[0]->GetUnsigned(), params_[4]->GetSigned());
+    const auto y = format_coordinate(params_[1]->GetUnsigned(), params_[5]->GetSigned());
+    const auto& triangle_id = FieldCodeGenerator::FormatValueOrVariable(
+      cg->GetFormatter(), params_[3]->GetUnsigned(), params_[6]->GetUnsigned()
+    );
+    code_gen->AddOutputLine((
+      boost::format("self.%1%:set_position_xy(%2%, %3%) -- triangle ID %4%")
+      % entity % x % y % triangle_id
+    ).str());
+}
+
 void FieldModelInstruction::ProcessMOVE(CodeGenerator* code_gen, const std::string& entity){
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
     const float scale = 128.0f * cg->GetScaleFactor();
@@ -691,6 +718,18 @@ void FieldModelInstruction::ProcessAXYZI(CodeGenerator* code_gen){
     ).str());
 }
 
+void FieldModelInstruction::ProcessGETAXY(CodeGenerator* code_gen){
+    // Stores the X and Y of an entity (given by its index in the field) in two variables, in
+    // field units. The script of a line, for example, keeps looking at where the player is.
+    FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
+    const float scale = 128.0f * cg->GetScaleFactor();
+    code_gen->AddOutputLine((
+      boost::format("getaxy(%1%, %2%, %3%, %4%, %5%, %6%)")
+      % params_[0]->GetSigned() % params_[1]->GetSigned() % params_[2]->GetSigned()
+      % params_[3]->GetSigned() % params_[4]->GetSigned() % scale
+    ).str());
+}
+
 void FieldModelInstruction::ProcessLADER(
   CodeGenerator* code_gen, const std::string& entity, int char_id
 ){
@@ -753,8 +792,10 @@ void FieldModelInstruction::ProcessSLIDR(CodeGenerator* code_gen, const std::str
 }
 
 void FieldModelInstruction::ProcessSOLID(CodeGenerator* code_gen, const std::string& entity){
+    // Lines (triggers) can run this opcode too, but they don't have a model to make solid, so
+    // the entity may not exist in the script.
     code_gen->AddOutputLine((
-      boost::format("self.%1%:set_solid(%2%)")
+      boost::format("if self.%1% ~= nil then self.%1%:set_solid(%2%) end")
       % entity % FieldCodeGenerator::FormatInvertedBool(params_[0]->GetUnsigned())
     ).str());
 }

+ 16 - 0
src/installer/decompiler/field/instruction/FieldModelInstruction.h

@@ -154,6 +154,12 @@ class FieldModelInstruction : public KernelCallInstruction{
 
         void ProcessXYZI(CodeGenerator* code_gen, const std::string& entity);
 
+        /**
+         * Processes a XYI opcode: sets the X and Y position of the entity (and its triangle).
+         * Unlike XYZI, it doesn't change the visibility of the entity.
+         */
+        void ProcessXYI(CodeGenerator* code_gen, const std::string& entity);
+
         void ProcessMOVE(CodeGenerator* code_gen, const std::string& entity);
 
         /**
@@ -432,6 +438,16 @@ class FieldModelInstruction : public KernelCallInstruction{
          */
         void ProcessAXYZI(CodeGenerator* code_gen);
 
+        /**
+         * Processes a GETAXY opcode: stores the X and Y of an entity in two variables.
+         *
+         * Opcode: 0xB8
+         * Short name: GETAXY
+         *
+         * @param[in,out] code_gen Code generator to append lines.
+         */
+        void ProcessGETAXY(CodeGenerator* code_gen);
+
         /**
          * Processes a LADER opcode.
          *

+ 34 - 30
src/installer/decompiler/field/instruction/FieldPartyInstruction.cpp

@@ -49,8 +49,18 @@ void FieldPartyInstruction::ProcessInst(
         case OPCODES::DMTRA: code_gen->WriteTodo(md.GetEntityName(), "DMTRA"); break;
         case OPCODES::CMTRA: code_gen->WriteTodo(md.GetEntityName(), "CMTRA"); break;
         case OPCODES::GETPC: code_gen->WriteTodo(md.GetEntityName(), "GETPC"); break;
-        case OPCODES::PRTYP: code_gen->WriteTodo(md.GetEntityName(), "PRTYP"); break;
-        case OPCODES::PRTYM: code_gen->WriteTodo(md.GetEntityName(), "PRTYM"); break;
+        case OPCODES::PRTYP:
+            // Adds a character to the party, in the first free place.
+            code_gen->AddOutputLine(
+              (boost::format("add_party_character(%1%)") % params_[0]->GetString()).str()
+            );
+            break;
+        case OPCODES::PRTYM:
+            // Removes a character from the party.
+            code_gen->AddOutputLine(
+              (boost::format("remove_party_character(%1%)") % params_[0]->GetString()).str()
+            );
+            break;
         case OPCODES::PRTYE: ProcessPRTYE(code_gen); break;
         case OPCODES::MMBUD: ProcessMMBUD(code_gen); break;
         case OPCODES::MMBLK: ProcessMMBLK(code_gen); break;
@@ -63,38 +73,32 @@ void FieldPartyInstruction::ProcessInst(
     }
 }
 
-void FieldPartyInstruction::ProcessGOLDU(CodeGenerator* code_gen){
-    if (params_[0]->GetUnsigned() == 0){
-        // Add by constant amount, params 1-4 are a dword with the amount.
-        // Quick and dirty hex operation:
-        long amount = params_[1]->GetUnsigned() + params_[2]->GetUnsigned() * 256
-          + params_[3]->GetUnsigned() * 256 * 256 + params_[4]->GetUnsigned() * 256 * 256 * 256;
-        code_gen->AddOutputLine((boost::format("Inventory.add_money(%1%)") % amount).str());
-    }
-    else{
-        // Add by variable amount, param 0 is bank and 1 is address. 2-4 are zeroes, ignored.
-        code_gen->AddOutputLine((
-          boost::format("Inventory.add_money(Banks[%1%][%2%])")
-            % params_[0]->GetUnsigned() % params_[1]->GetUnsigned()
-        ).str());
+namespace{
+    /**
+     * Writes the amount of gil of a GOLDU or GOLDD.
+     *
+     * The opcode is read as a byte of two nibbles (two parameters, the bank is the first) followed
+     * by four bytes. With a bank of zero, the four bytes are the amount, as a little endian dword.
+     * Otherwise, the first of the four bytes is the address in the bank, and the rest are zeroes.
+     */
+    std::string GilAmount(const std::vector<ValuePtr>& params){
+        if (params[0]->GetUnsigned() == 0){
+            const long amount = params[2]->GetUnsigned() + params[3]->GetUnsigned() * 256
+              + params[4]->GetUnsigned() * 256 * 256 + params[5]->GetUnsigned() * 256 * 256 * 256;
+            return std::to_string(amount);
+        }
+        return (
+          boost::format("Banks[%1%][%2%]") % params[0]->GetUnsigned() % params[2]->GetUnsigned()
+        ).str();
     }
 }
 
+void FieldPartyInstruction::ProcessGOLDU(CodeGenerator* code_gen){
+    code_gen->AddOutputLine("Inventory.add_money(" + GilAmount(params_) + ")");
+}
+
 void FieldPartyInstruction::ProcessGOLDD(CodeGenerator* code_gen){
-    if (params_[0]->GetUnsigned() == 0){
-        // Add by constant amount, params 1-4 are a dword with the amount.
-        // Quick and dirty hex operation:
-        long amount = params_[1]->GetUnsigned() + params_[2]->GetUnsigned() * 256
-          + params_[3]->GetUnsigned() * 256 * 256 + params_[4]->GetUnsigned() * 256 * 256 * 256;
-        code_gen->AddOutputLine((boost::format("Inventory.add_money(%1%)") % amount).str());
-    }
-    else{
-        // Add by variable amount, param 0 is bank and 1 is address. 2-4 are zeroes, ignored.
-        code_gen->AddOutputLine((
-          boost::format("Inventory.add_money(Banks[%1%][%2%])")
-            % params_[0]->GetUnsigned() % params_[1]->GetUnsigned()
-        ).str());
-    }
+    code_gen->AddOutputLine("Inventory.remove_money(" + GilAmount(params_) + ")");
 }
 
 void FieldPartyInstruction::ProcessSTITM(CodeGenerator* code_gen){

+ 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)
 
 
-# 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
+    common/DataDirectory.cpp
+    common/OgrePlugins.cpp
+    ${CMAKE_SOURCE_DIR}/src/common/DataDirectory.cpp
+    ${CMAKE_SOURCE_DIR}/src/common/OgrePlugins.cpp
     installer/InstallLogic.cpp
     installer/ExternalTools.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")
 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)

+ 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]
-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
-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;