From 7ee008e150bc5bcf76082d726f719ee0fdfda982 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Wed, 11 Feb 2026 02:37:39 -0600 Subject: Fluorine Manager: full Linux port of Mod Organizer 2 Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 --- .../game_bethesda/src/games/enderal/CMakeLists.txt | 24 ++ .../src/games/enderal/enderalbsainvalidation.cpp | 16 + .../src/games/enderal/enderalbsainvalidation.h | 20 + .../src/games/enderal/enderaldataarchives.cpp | 42 ++ .../src/games/enderal/enderaldataarchives.h | 23 ++ .../src/games/enderal/enderalgameplugins.cpp | 123 ++++++ .../src/games/enderal/enderalgameplugins.h | 23 ++ .../src/games/enderal/enderallocalsavegames.cpp | 24 ++ .../src/games/enderal/enderallocalsavegames.h | 35 ++ .../src/games/enderal/enderalmoddatachecker.h | 49 +++ .../src/games/enderal/enderalmoddatacontent.h | 14 + .../src/games/enderal/enderalsavegame.cpp | 101 +++++ .../src/games/enderal/enderalsavegame.h | 23 ++ .../src/games/enderal/enderalscriptextender.cpp | 18 + .../src/games/enderal/enderalscriptextender.h | 17 + .../src/games/enderal/game_enderal_en.ts | 17 + .../src/games/enderal/gameenderal.cpp | 267 +++++++++++++ libs/game_bethesda/src/games/enderal/gameenderal.h | 61 +++ .../src/games/enderal/gameenderal.json | 1 + .../src/games/enderalse/CMakeLists.txt | 24 ++ .../src/games/enderalse/enderalsedataarchives.cpp | 64 +++ .../src/games/enderalse/enderalsedataarchives.h | 26 ++ .../src/games/enderalse/enderalsegameplugins.cpp | 82 ++++ .../src/games/enderalse/enderalsegameplugins.h | 19 + .../games/enderalse/enderalselocalsavegames.cpp | 24 ++ .../src/games/enderalse/enderalselocalsavegames.h | 17 + .../src/games/enderalse/enderalsemoddatachecker.h | 31 ++ .../src/games/enderalse/enderalsemoddatacontent.h | 21 + .../src/games/enderalse/enderalsesavegame.cpp | 148 +++++++ .../src/games/enderalse/enderalsesavegame.h | 28 ++ .../games/enderalse/enderalsescriptextender.cpp | 18 + .../src/games/enderalse/enderalsescriptextender.h | 17 + .../src/games/enderalse/enderalseunmanagedmods.cpp | 29 ++ .../src/games/enderalse/enderalseunmanagedmods.h | 16 + .../src/games/enderalse/game_enderalse_en.ts | 25 ++ .../src/games/enderalse/gameenderalse.cpp | 347 ++++++++++++++++ .../src/games/enderalse/gameenderalse.h | 75 ++++ .../src/games/enderalse/gameenderalse.json | 1 + .../src/games/fallout3/CMakeLists.txt | 20 + .../src/games/fallout3/fallout3bsainvalidation.cpp | 16 + .../src/games/fallout3/fallout3bsainvalidation.h | 20 + .../src/games/fallout3/fallout3dataarchives.cpp | 33 ++ .../src/games/fallout3/fallout3dataarchives.h | 20 + .../src/games/fallout3/fallout3moddatachecker.h | 31 ++ .../src/games/fallout3/fallout3moddatacontent.h | 22 ++ .../src/games/fallout3/fallout3savegame.cpp | 72 ++++ .../src/games/fallout3/fallout3savegame.h | 23 ++ .../src/games/fallout3/fallout3scriptextender.cpp | 18 + .../src/games/fallout3/fallout3scriptextender.h | 17 + .../src/games/fallout3/game_fallout3_en.ts | 17 + .../src/games/fallout3/gamefallout3.cpp | 279 +++++++++++++ .../src/games/fallout3/gamefallout3.h | 58 +++ .../src/games/fallout3/gamefallout3.json | 1 + .../src/games/fallout4/CMakeLists.txt | 22 ++ libs/game_bethesda/src/games/fallout4/fallout4.qrc | 5 + .../src/games/fallout4/fallout4bsainvalidation.cpp | 68 ++++ .../src/games/fallout4/fallout4bsainvalidation.h | 33 ++ .../src/games/fallout4/fallout4dataarchives.cpp | 48 +++ .../src/games/fallout4/fallout4dataarchives.h | 27 ++ .../src/games/fallout4/fallout4moddatachecker.h | 29 ++ .../src/games/fallout4/fallout4moddatacontent.h | 44 +++ .../src/games/fallout4/fallout4savegame.cpp | 106 +++++ .../src/games/fallout4/fallout4savegame.h | 23 ++ .../src/games/fallout4/fallout4scriptextender.cpp | 18 + .../src/games/fallout4/fallout4scriptextender.h | 17 + .../src/games/fallout4/fallout4unmanagedmods.cpp | 63 +++ .../src/games/fallout4/fallout4unmanagedmods.h | 18 + .../src/games/fallout4/game_fallout4_en.ts | 28 ++ .../src/games/fallout4/gamefallout4.cpp | 334 ++++++++++++++++ .../src/games/fallout4/gamefallout4.h | 73 ++++ .../src/games/fallout4/gamefallout4.json | 1 + libs/game_bethesda/src/games/fallout4/splash.png | Bin 0 -> 52981 bytes .../src/games/fallout4london/CMakeLists.txt | 20 + .../src/games/fallout4london/fo4london.qrc | 5 + .../fallout4london/fo4londonbsainvalidation.cpp | 68 ++++ .../fallout4london/fo4londonbsainvalidation.h | 33 ++ .../games/fallout4london/fo4londondataarchives.cpp | 48 +++ .../games/fallout4london/fo4londondataarchives.h | 27 ++ .../games/fallout4london/fo4londonmoddatachecker.h | 29 ++ .../games/fallout4london/fo4londonmoddatacontent.h | 44 +++ .../src/games/fallout4london/fo4londonsavegame.cpp | 108 +++++ .../src/games/fallout4london/fo4londonsavegame.h | 23 ++ .../fallout4london/fo4londonscriptextender.cpp | 18 + .../games/fallout4london/fo4londonscriptextender.h | 17 + .../fallout4london/fo4londonunmanagedmods.cpp | 63 +++ .../games/fallout4london/fo4londonunmanagedmods.h | 18 + .../src/games/fallout4london/game_fo4london_en.ts | 27 ++ .../src/games/fallout4london/gamefo4london.cpp | 366 +++++++++++++++++ .../src/games/fallout4london/gamefo4london.h | 78 ++++ .../src/games/fallout4london/gamefo4london.json | 1 + .../src/games/fallout4london/splash.png | Bin 0 -> 52981 bytes .../src/games/fallout4vr/CMakeLists.txt | 22 ++ .../src/games/fallout4vr/fallout4vr.qrc | 5 + .../games/fallout4vr/fallout4vrdataarchives.cpp | 49 +++ .../src/games/fallout4vr/fallout4vrdataarchives.h | 27 ++ .../src/games/fallout4vr/fallout4vrgameplugins.cpp | 15 + .../src/games/fallout4vr/fallout4vrgameplugins.h | 19 + .../games/fallout4vr/fallout4vrmoddatachecker.h | 30 ++ .../games/fallout4vr/fallout4vrmoddatacontent.h | 44 +++ .../src/games/fallout4vr/fallout4vrsavegame.cpp | 108 +++++ .../src/games/fallout4vr/fallout4vrsavegame.h | 23 ++ .../games/fallout4vr/fallout4vrscriptextender.cpp | 18 + .../games/fallout4vr/fallout4vrscriptextender.h | 17 + .../games/fallout4vr/fallout4vrunmanagedmods.cpp | 63 +++ .../src/games/fallout4vr/fallout4vrunmanagedmods.h | 18 + .../src/games/fallout4vr/game_fallout4vr_en.ts | 18 + .../src/games/fallout4vr/gamefallout4vr.cpp | 251 ++++++++++++ .../src/games/fallout4vr/gamefallout4vr.h | 59 +++ .../src/games/fallout4vr/gamefallout4vr.json | 1 + libs/game_bethesda/src/games/fallout4vr/splash.png | Bin 0 -> 52981 bytes .../src/games/fallout76/CMakeLists.txt | 22 ++ .../src/games/fallout76/fallout76.qrc | 5 + .../src/games/fallout76/fallout76dataarchives.cpp | 150 +++++++ .../src/games/fallout76/fallout76dataarchives.h | 33 ++ .../src/games/fallout76/fallout76moddatachecker.h | 44 +++ .../src/games/fallout76/fallout76moddatacontent.h | 44 +++ .../src/games/fallout76/fallout76savegame.cpp | 106 +++++ .../src/games/fallout76/fallout76savegame.h | 23 ++ .../src/games/fallout76/fallout76savegameinfo.cpp | 10 + .../src/games/fallout76/fallout76savegameinfo.h | 15 + .../games/fallout76/fallout76scriptextender.cpp | 18 + .../src/games/fallout76/fallout76scriptextender.h | 17 + .../src/games/fallout76/fallout76unmanagedmods.cpp | 46 +++ .../src/games/fallout76/fallout76unmanagedmods.h | 18 + .../src/games/fallout76/game_fallout76_en.ts | 13 + .../src/games/fallout76/gamefallout76.cpp | 227 +++++++++++ .../src/games/fallout76/gamefallout76.h | 55 +++ .../src/games/fallout76/gamefallout76.json | 1 + libs/game_bethesda/src/games/fallout76/splash.png | Bin 0 -> 52981 bytes .../src/games/falloutnv/CMakeLists.txt | 31 ++ .../games/falloutnv/falloutnvbsainvalidation.cpp | 16 + .../src/games/falloutnv/falloutnvbsainvalidation.h | 20 + .../src/games/falloutnv/falloutnvdataarchives.cpp | 32 ++ .../src/games/falloutnv/falloutnvdataarchives.h | 23 ++ .../src/games/falloutnv/falloutnvmoddatachecker.h | 51 +++ .../src/games/falloutnv/falloutnvmoddatacontent.h | 22 ++ .../src/games/falloutnv/falloutnvsavegame.cpp | 77 ++++ .../src/games/falloutnv/falloutnvsavegame.h | 23 ++ .../games/falloutnv/falloutnvscriptextender.cpp | 18 + .../src/games/falloutnv/falloutnvscriptextender.h | 17 + .../src/games/falloutnv/game_falloutNV_en.ts | 22 ++ .../src/games/falloutnv/gamefalloutnv.cpp | 334 ++++++++++++++++ .../src/games/falloutnv/gamefalloutnv.h | 70 ++++ .../src/games/falloutnv/gamefalloutnv.json | 1 + .../src/games/morrowind/CMakeLists.txt | 27 ++ .../src/games/morrowind/game_morrowind_en.ts | 69 ++++ .../src/games/morrowind/gamemorrowind.cpp | 216 ++++++++++ .../src/games/morrowind/gamemorrowind.h | 62 +++ .../src/games/morrowind/gamemorrowind.json | 1 + .../src/games/morrowind/morrowind.qrc | 5 + .../games/morrowind/morrowindbsainvalidation.cpp | 16 + .../src/games/morrowind/morrowindbsainvalidation.h | 20 + .../src/games/morrowind/morrowinddataarchives.cpp | 71 ++++ .../src/games/morrowind/morrowinddataarchives.h | 28 ++ .../src/games/morrowind/morrowindgameplugins.cpp | 197 +++++++++ .../src/games/morrowind/morrowindgameplugins.h | 28 ++ .../games/morrowind/morrowindlocalsavegames.cpp | 56 +++ .../src/games/morrowind/morrowindlocalsavegames.h | 41 ++ .../src/games/morrowind/morrowindmoddatachecker.h | 26 ++ .../src/games/morrowind/morrowindmoddatacontent.h | 24 ++ .../src/games/morrowind/morrowindsavegame.cpp | 183 +++++++++ .../src/games/morrowind/morrowindsavegame.h | 57 +++ .../src/games/morrowind/morrowindsavegameinfo.cpp | 18 + .../src/games/morrowind/morrowindsavegameinfo.h | 22 ++ .../morrowind/morrowindsavegameinfowidget.cpp | 123 ++++++ .../games/morrowind/morrowindsavegameinfowidget.h | 31 ++ .../games/morrowind/morrowindsavegameinfowidget.ui | 248 ++++++++++++ libs/game_bethesda/src/games/morrowind/splash.png | Bin 0 -> 83336 bytes libs/game_bethesda/src/games/nehrim/CMakeLists.txt | 21 + .../src/games/nehrim/game_nehrim_en.ts | 17 + libs/game_bethesda/src/games/nehrim/gamenehrim.cpp | 205 ++++++++++ libs/game_bethesda/src/games/nehrim/gamenehrim.h | 56 +++ .../game_bethesda/src/games/nehrim/gamenehrim.json | 1 + .../src/games/nehrim/nehrimbsainvalidation.cpp | 16 + .../src/games/nehrim/nehrimbsainvalidation.h | 20 + .../src/games/nehrim/nehrimdataarchives.cpp | 31 ++ .../src/games/nehrim/nehrimdataarchives.h | 23 ++ .../src/games/nehrim/nehrimmoddatachecker.cpp | 30 ++ .../src/games/nehrim/nehrimmoddatachecker.h | 35 ++ .../src/games/nehrim/nehrimmoddatacontent.h | 22 ++ .../src/games/nehrim/nehrimsavegame.cpp | 72 ++++ .../src/games/nehrim/nehrimsavegame.h | 21 + .../src/games/nehrim/nehrimscriptextender.cpp | 20 + .../src/games/nehrim/nehrimscriptextender.h | 18 + .../src/games/oblivion/CMakeLists.txt | 21 + .../src/games/oblivion/game_oblivion_en.ts | 17 + .../src/games/oblivion/gameoblivion.cpp | 213 ++++++++++ .../src/games/oblivion/gameoblivion.h | 51 +++ .../src/games/oblivion/gameoblivion.json | 1 + .../src/games/oblivion/oblivionbsainvalidation.cpp | 16 + .../src/games/oblivion/oblivionbsainvalidation.h | 20 + .../src/games/oblivion/obliviondataarchives.cpp | 32 ++ .../src/games/oblivion/obliviondataarchives.h | 23 ++ .../src/games/oblivion/oblivionmoddatachecker.cpp | 30 ++ .../src/games/oblivion/oblivionmoddatachecker.h | 35 ++ .../src/games/oblivion/oblivionmoddatacontent.h | 22 ++ .../src/games/oblivion/oblivionsavegame.cpp | 74 ++++ .../src/games/oblivion/oblivionsavegame.h | 21 + .../src/games/oblivion/oblivionscriptextender.cpp | 20 + .../src/games/oblivion/oblivionscriptextender.h | 18 + libs/game_bethesda/src/games/skyrim/CMakeLists.txt | 22 ++ .../src/games/skyrim/game_skyrim_en.ts | 17 + libs/game_bethesda/src/games/skyrim/gameskyrim.cpp | 263 +++++++++++++ libs/game_bethesda/src/games/skyrim/gameskyrim.h | 55 +++ .../game_bethesda/src/games/skyrim/gameskyrim.json | 1 + .../src/games/skyrim/skyrimbsainvalidation.cpp | 16 + .../src/games/skyrim/skyrimbsainvalidation.h | 20 + .../src/games/skyrim/skyrimdataarchives.cpp | 42 ++ .../src/games/skyrim/skyrimdataarchives.h | 23 ++ .../src/games/skyrim/skyrimgameplugins.cpp | 123 ++++++ .../src/games/skyrim/skyrimgameplugins.h | 23 ++ .../src/games/skyrim/skyrimmoddatachecker.h | 50 +++ .../src/games/skyrim/skyrimmoddatacontent.h | 14 + .../src/games/skyrim/skyrimsavegame.cpp | 105 +++++ .../src/games/skyrim/skyrimsavegame.h | 28 ++ .../src/games/skyrim/skyrimscriptextender.cpp | 18 + .../src/games/skyrim/skyrimscriptextender.h | 17 + .../src/games/skyrimse/CMakeLists.txt | 36 ++ .../src/games/skyrimse/game_skyrimse_en.ts | 17 + .../src/games/skyrimse/gameskyrimse.cpp | 359 +++++++++++++++++ .../src/games/skyrimse/gameskyrimse.h | 72 ++++ .../src/games/skyrimse/skyrimsedataarchives.cpp | 44 +++ .../src/games/skyrimse/skyrimsedataarchives.h | 25 ++ .../src/games/skyrimse/skyrimsemoddatachecker.h | 42 ++ .../src/games/skyrimse/skyrimsemoddatacontent.h | 21 + .../src/games/skyrimse/skyrimsesavegame.cpp | 157 ++++++++ .../src/games/skyrimse/skyrimsesavegame.h | 28 ++ .../src/games/skyrimse/skyrimsescriptextender.cpp | 18 + .../src/games/skyrimse/skyrimsescriptextender.h | 17 + .../src/games/skyrimse/skyrimseunmanagedmods.cpp | 29 ++ .../src/games/skyrimse/skyrimseunmanagedmods.h | 16 + .../src/games/skyrimvr/CMakeLists.txt | 22 ++ .../src/games/skyrimvr/game_skyrimvr_en.ts | 17 + .../src/games/skyrimvr/gameskyrimvr.cpp | 306 ++++++++++++++ .../src/games/skyrimvr/gameskyrimvr.h | 70 ++++ .../src/games/skyrimvr/gameskyrimvr.json | 1 + .../src/games/skyrimvr/skyrimvrdataarchives.cpp | 45 +++ .../src/games/skyrimvr/skyrimvrdataarchives.h | 26 ++ .../src/games/skyrimvr/skyrimvrgameplugins.cpp | 15 + .../src/games/skyrimvr/skyrimvrgameplugins.h | 19 + .../src/games/skyrimvr/skyrimvrmoddatachecker.h | 31 ++ .../src/games/skyrimvr/skyrimvrmoddatacontent.h | 21 + .../src/games/skyrimvr/skyrimvrsavegame.cpp | 149 +++++++ .../src/games/skyrimvr/skyrimvrsavegame.h | 24 ++ .../src/games/skyrimvr/skyrimvrscriptextender.cpp | 18 + .../src/games/skyrimvr/skyrimvrscriptextender.h | 17 + .../src/games/skyrimvr/skyrimvrunmanagedmods.cpp | 29 ++ .../src/games/skyrimvr/skyrimvrunmanagedmods.h | 16 + .../src/games/starfield/CMakeLists.txt | 24 ++ .../src/games/starfield/game_starfield_en.ts | 73 ++++ .../src/games/starfield/gamestarfield.cpp | 438 +++++++++++++++++++++ .../src/games/starfield/gamestarfield.h | 83 ++++ .../src/games/starfield/gamestarfield.json | 1 + .../games/starfield/starfieldbsainvalidation.cpp | 68 ++++ .../src/games/starfield/starfieldbsainvalidation.h | 33 ++ .../src/games/starfield/starfielddataarchives.cpp | 98 +++++ .../src/games/starfield/starfielddataarchives.h | 28 ++ .../src/games/starfield/starfieldgameplugins.cpp | 35 ++ .../src/games/starfield/starfieldgameplugins.h | 23 ++ .../src/games/starfield/starfieldmoddatachecker.h | 29 ++ .../src/games/starfield/starfieldmoddatacontent.h | 55 +++ .../src/games/starfield/starfieldsavegame.cpp | 157 ++++++++ .../src/games/starfield/starfieldsavegame.h | 27 ++ .../games/starfield/starfieldscriptextender.cpp | 18 + .../src/games/starfield/starfieldscriptextender.h | 17 + .../src/games/starfield/starfieldunmanagedmods.cpp | 132 +++++++ .../src/games/starfield/starfieldunmanagedmods.h | 36 ++ libs/game_bethesda/src/games/ttw/CMakeLists.txt | 20 + .../src/games/ttw/falloutttwbsainvalidation.cpp | 16 + .../src/games/ttw/falloutttwbsainvalidation.h | 20 + .../src/games/ttw/falloutttwdataarchives.cpp | 31 ++ .../src/games/ttw/falloutttwdataarchives.h | 23 ++ .../src/games/ttw/falloutttwmoddatachecker.h | 51 +++ .../src/games/ttw/falloutttwmoddatacontent.h | 22 ++ .../src/games/ttw/falloutttwsavegame.cpp | 79 ++++ .../src/games/ttw/falloutttwsavegame.h | 23 ++ .../src/games/ttw/falloutttwscriptextender.cpp | 18 + .../src/games/ttw/falloutttwscriptextender.h | 17 + libs/game_bethesda/src/games/ttw/game_ttw_en.ts | 22 ++ .../game_bethesda/src/games/ttw/gamefalloutttw.cpp | 364 +++++++++++++++++ libs/game_bethesda/src/games/ttw/gamefalloutttw.h | 77 ++++ .../src/games/ttw/gamefalloutttw.json | 1 + 282 files changed, 14392 insertions(+) create mode 100644 libs/game_bethesda/src/games/enderal/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderalbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderaldataarchives.h create mode 100644 libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderalgameplugins.h create mode 100644 libs/game_bethesda/src/games/enderal/enderallocalsavegames.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderallocalsavegames.h create mode 100644 libs/game_bethesda/src/games/enderal/enderalmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/enderal/enderalmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/enderal/enderalsavegame.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderalsavegame.h create mode 100644 libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderalscriptextender.h create mode 100644 libs/game_bethesda/src/games/enderal/game_enderal_en.ts create mode 100644 libs/game_bethesda/src/games/enderal/gameenderal.cpp create mode 100644 libs/game_bethesda/src/games/enderal/gameenderal.h create mode 100644 libs/game_bethesda/src/games/enderal/gameenderal.json create mode 100644 libs/game_bethesda/src/games/enderalse/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsedataarchives.h create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsegameplugins.h create mode 100644 libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.h create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsemoddatachecker.h create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsemoddatacontent.h create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsesavegame.h create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsescriptextender.h create mode 100644 libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.h create mode 100644 libs/game_bethesda/src/games/enderalse/game_enderalse_en.ts create mode 100644 libs/game_bethesda/src/games/enderalse/gameenderalse.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/gameenderalse.h create mode 100644 libs/game_bethesda/src/games/enderalse/gameenderalse.json create mode 100644 libs/game_bethesda/src/games/fallout3/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.h create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3dataarchives.h create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3moddatachecker.h create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3moddatacontent.h create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3savegame.cpp create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3savegame.h create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3scriptextender.cpp create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3scriptextender.h create mode 100644 libs/game_bethesda/src/games/fallout3/game_fallout3_en.ts create mode 100644 libs/game_bethesda/src/games/fallout3/gamefallout3.cpp create mode 100644 libs/game_bethesda/src/games/fallout3/gamefallout3.h create mode 100644 libs/game_bethesda/src/games/fallout3/gamefallout3.json create mode 100644 libs/game_bethesda/src/games/fallout4/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4.qrc create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.h create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4dataarchives.h create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4moddatachecker.h create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4moddatacontent.h create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4savegame.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4savegame.h create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4scriptextender.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4scriptextender.h create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.h create mode 100644 libs/game_bethesda/src/games/fallout4/game_fallout4_en.ts create mode 100644 libs/game_bethesda/src/games/fallout4/gamefallout4.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/gamefallout4.h create mode 100644 libs/game_bethesda/src/games/fallout4/gamefallout4.json create mode 100644 libs/game_bethesda/src/games/fallout4/splash.png create mode 100644 libs/game_bethesda/src/games/fallout4london/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4london.qrc create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.h create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonsavegame.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonsavegame.h create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonscriptextender.h create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonunmanagedmods.h create mode 100644 libs/game_bethesda/src/games/fallout4london/game_fo4london_en.ts create mode 100644 libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/gamefo4london.h create mode 100644 libs/game_bethesda/src/games/fallout4london/gamefo4london.json create mode 100644 libs/game_bethesda/src/games/fallout4london/splash.png create mode 100644 libs/game_bethesda/src/games/fallout4vr/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vr.qrc create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.h create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrgameplugins.h create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrsavegame.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrsavegame.h create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrscriptextender.h create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrunmanagedmods.h create mode 100644 libs/game_bethesda/src/games/fallout4vr/game_fallout4vr_en.ts create mode 100644 libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.h create mode 100644 libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.json create mode 100644 libs/game_bethesda/src/games/fallout4vr/splash.png create mode 100644 libs/game_bethesda/src/games/fallout76/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76.qrc create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76dataarchives.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76dataarchives.h create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76moddatachecker.h create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76moddatacontent.h create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76savegame.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76savegame.h create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76savegameinfo.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76savegameinfo.h create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76scriptextender.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76scriptextender.h create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76unmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76unmanagedmods.h create mode 100644 libs/game_bethesda/src/games/fallout76/game_fallout76_en.ts create mode 100644 libs/game_bethesda/src/games/fallout76/gamefallout76.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/gamefallout76.h create mode 100644 libs/game_bethesda/src/games/fallout76/gamefallout76.json create mode 100644 libs/game_bethesda/src/games/fallout76/splash.png create mode 100644 libs/game_bethesda/src/games/falloutnv/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.h create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvsavegame.cpp create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvsavegame.h create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvscriptextender.h create mode 100644 libs/game_bethesda/src/games/falloutnv/game_falloutNV_en.ts create mode 100644 libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp create mode 100644 libs/game_bethesda/src/games/falloutnv/gamefalloutnv.h create mode 100644 libs/game_bethesda/src/games/falloutnv/gamefalloutnv.json create mode 100644 libs/game_bethesda/src/games/morrowind/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/morrowind/game_morrowind_en.ts create mode 100644 libs/game_bethesda/src/games/morrowind/gamemorrowind.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/gamemorrowind.h create mode 100644 libs/game_bethesda/src/games/morrowind/gamemorrowind.json create mode 100644 libs/game_bethesda/src/games/morrowind/morrowind.qrc create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowinddataarchives.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowinddataarchives.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindgameplugins.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegame.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegame.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.ui create mode 100644 libs/game_bethesda/src/games/morrowind/splash.png create mode 100644 libs/game_bethesda/src/games/nehrim/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/nehrim/game_nehrim_en.ts create mode 100644 libs/game_bethesda/src/games/nehrim/gamenehrim.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/gamenehrim.h create mode 100644 libs/game_bethesda/src/games/nehrim/gamenehrim.json create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimdataarchives.h create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimmoddatachecker.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimsavegame.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimsavegame.h create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimscriptextender.h create mode 100644 libs/game_bethesda/src/games/oblivion/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/oblivion/game_oblivion_en.ts create mode 100644 libs/game_bethesda/src/games/oblivion/gameoblivion.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/gameoblivion.h create mode 100644 libs/game_bethesda/src/games/oblivion/gameoblivion.json create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/oblivion/obliviondataarchives.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/obliviondataarchives.h create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionmoddatachecker.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionsavegame.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionsavegame.h create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionscriptextender.h create mode 100644 libs/game_bethesda/src/games/skyrim/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/skyrim/game_skyrim_en.ts create mode 100644 libs/game_bethesda/src/games/skyrim/gameskyrim.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/gameskyrim.h create mode 100644 libs/game_bethesda/src/games/skyrim/gameskyrim.json create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimdataarchives.h create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimgameplugins.h create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimsavegame.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimsavegame.h create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimscriptextender.h create mode 100644 libs/game_bethesda/src/games/skyrimse/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/skyrimse/game_skyrimse_en.ts create mode 100644 libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp create mode 100644 libs/game_bethesda/src/games/skyrimse/gameskyrimse.h create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.cpp create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.h create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsemoddatachecker.h create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsemoddatacontent.h create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.cpp create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.h create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.cpp create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.h create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.h create mode 100644 libs/game_bethesda/src/games/skyrimvr/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/skyrimvr/game_skyrimvr_en.ts create mode 100644 libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.h create mode 100644 libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.json create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.h create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrgameplugins.h create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrsavegame.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrsavegame.h create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrscriptextender.h create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrunmanagedmods.h create mode 100644 libs/game_bethesda/src/games/starfield/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/starfield/game_starfield_en.ts create mode 100644 libs/game_bethesda/src/games/starfield/gamestarfield.cpp create mode 100644 libs/game_bethesda/src/games/starfield/gamestarfield.h create mode 100644 libs/game_bethesda/src/games/starfield/gamestarfield.json create mode 100644 libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/starfield/starfielddataarchives.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfielddataarchives.h create mode 100644 libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfieldgameplugins.h create mode 100644 libs/game_bethesda/src/games/starfield/starfieldmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/starfield/starfieldmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/starfield/starfieldsavegame.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfieldsavegame.h create mode 100644 libs/game_bethesda/src/games/starfield/starfieldscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfieldscriptextender.h create mode 100644 libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.h create mode 100644 libs/game_bethesda/src/games/ttw/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwdataarchives.h create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwsavegame.cpp create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwsavegame.h create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwscriptextender.h create mode 100644 libs/game_bethesda/src/games/ttw/game_ttw_en.ts create mode 100644 libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp create mode 100644 libs/game_bethesda/src/games/ttw/gamefalloutttw.h create mode 100644 libs/game_bethesda/src/games/ttw/gamefalloutttw.json (limited to 'libs/game_bethesda/src/games') diff --git a/libs/game_bethesda/src/games/enderal/CMakeLists.txt b/libs/game_bethesda/src/games/enderal/CMakeLists.txt new file mode 100644 index 0000000..348429c --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_enderal SHARED + enderalbsainvalidation.cpp + enderalbsainvalidation.h + enderaldataarchives.cpp + enderaldataarchives.h + enderalgameplugins.cpp + enderalgameplugins.h + enderallocalsavegames.cpp + enderallocalsavegames.h + enderalmoddatachecker.h + enderalmoddatacontent.h + enderalsavegame.cpp + enderalsavegame.h + enderalscriptextender.cpp + enderalscriptextender.h + gameenderal.cpp + gameenderal.h +) +mo2_configure_plugin(game_enderal NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_enderal PRIVATE game_creation) +mo2_install_plugin(game_enderal) diff --git a/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp b/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp new file mode 100644 index 0000000..5564075 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "enderalbsainvalidation.h" + +EnderalBSAInvalidation::EnderalBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "enderal.ini", game) +{} + +QString EnderalBSAInvalidation::invalidationBSAName() const +{ + return "Enderal - Invalidation.bsa"; +} + +unsigned long EnderalBSAInvalidation::bsaVersion() const +{ + return 0x68; +} diff --git a/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.h b/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.h new file mode 100644 index 0000000..721da3e --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.h @@ -0,0 +1,20 @@ +#ifndef ENDERALBSAINVALIDATION_H +#define ENDERALBSAINVALIDATION_H + +#include "enderaldataarchives.h" +#include "gamebryobsainvalidation.h" + +#include + +class EnderalBSAInvalidation : public GamebryoBSAInvalidation +{ +public: + EnderalBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; +}; + +#endif // ENDERALBSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp b/libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp new file mode 100644 index 0000000..3ac4ee0 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp @@ -0,0 +1,42 @@ +#include "enderaldataarchives.h" +#include + +QStringList EnderalDataArchives::vanillaArchives() const +{ + return {"Skyrim - Misc.bsa", "Skyrim - Shaders.bsa", "Skyrim - Textures.bsa", + "Skyrim - Interface.bsa", "Skyrim - Animations.bsa", "Skyrim - Meshes.bsa", + "Skyrim - Sounds.bsa", "E - Meshes.bsa", "E - Music.bsa", + "E - Scripts.bsa", "E - Sounds.bsa", "E - Textures1.bsa", + "E - Textures2.bsa", "E - Textures3.bsa", "L - Textures.bsa", + "L - Voices.bsa"}; +} + +QStringList EnderalDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("enderal.ini") + : localGameDirectory().absoluteFilePath("enderal.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + + return result; +} + +void EnderalDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("enderal.ini") + : localGameDirectory().absoluteFilePath("enderal.ini"); + if (list.length() > 255) { + int splitIdx = list.lastIndexOf(",", 256); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/enderal/enderaldataarchives.h b/libs/game_bethesda/src/games/enderal/enderaldataarchives.h new file mode 100644 index 0000000..cea1a8b --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderaldataarchives.h @@ -0,0 +1,23 @@ +#ifndef ENDERALDATAARCHIVES_H +#define ENDERALDATAARCHIVES_H + +#include +#include +#include +#include +#include + +class EnderalDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // ENDERALDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp b/libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp new file mode 100644 index 0000000..cd8048e --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp @@ -0,0 +1,123 @@ +#include "enderalgameplugins.h" +#include +#include +#include +#include + +#include +#include +#include + +using MOBase::IOrganizer; +using MOBase::IPluginGame; +using MOBase::IPluginList; +using MOBase::reportError; +using MOBase::SafeWriteFile; + +EnderalGamePlugins::EnderalGamePlugins(IOrganizer* organizer) + : GamebryoGamePlugins(organizer) +{} + +void EnderalGamePlugins::readPluginLists(MOBase::IPluginList* pluginList) +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || + QFileInfo(loadOrderPath).lastModified() > m_LastRead; + bool pluginsIsNew = + !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; + + if (pluginsIsNew && !loadOrderIsNew) { + // If the plugins is new but not loadorder, we must reparse the load order from the + // plugin files + + // removed because returned loadorder was incorrect and did not account for plugins + // that were already disabled before. + /*QStringList loadOrder = readPluginList(pluginList); + pluginList->setLoadOrder(loadOrder);*/ + + // Fix me: we are ignoring order changes in plugins.txt favouring loadorder.txt in + // all cases (plugins.txt should have precedence) + QStringList loadOrder = readLoadOrderList(pluginList, loadOrderPath); + pluginList->setLoadOrder(loadOrder); + readPluginList(pluginList); + } else { + // read both files if they are both new or both older than the last read + QStringList loadOrder = readLoadOrderList(pluginList, loadOrderPath); + pluginList->setLoadOrder(loadOrder); + readPluginList(pluginList); + } + + m_LastRead = QDateTime::currentDateTime(); +} + +// TODO: return value is incorrect and should be ignored (it's not currently used +QStringList EnderalGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + QStringList plugins = pluginList->pluginNames(); + QStringList primaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList loadOrder(plugins); + + for (const QString& pluginName : primaryPlugins) { + if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + } + } + + // Do not sort the primary plugins. Their load order should be locked as defined in + // "primaryPlugins". + const QStringList pluginsClone(plugins); + for (QString plugin : pluginsClone) { + if (primaryPlugins.contains(plugin, Qt::CaseInsensitive)) + plugins.removeAll(plugin); + } + + // Determine plugin active state by the plugins.txt file. + bool pluginsTxtExists = true; + QString filePath = organizer()->profile()->absolutePath() + "/plugins.txt"; + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + pluginsTxtExists = false; + } + ON_BLOCK_EXIT([&]() { + qDebug("close %s", qUtf8Printable(filePath)); + file.close(); + }); + + if (file.size() == 0) { + // MO stores at least a header in the file. if it's completely empty the + // file is broken + pluginsTxtExists = false; + } + + if (pluginsTxtExists) { + while (!file.atEnd()) { + QByteArray line = file.readLine(); + QString pluginName; + if ((line.size() > 0) && (line.at(0) != '#')) { + pluginName = QStringEncoder(QStringConverter::Encoding::System) + .encode(line.trimmed().constData()); + } + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + plugins.removeAll(pluginName); + // we already have the old loadorder and we ignore the positions in plugins.txt + // (needs fix) loadOrder.append(pluginName); + } + } + + file.close(); + + // we removed each plugin found in the file, so what's left are inactive mods + for (const QString& pluginName : plugins) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } else { + for (const QString& pluginName : plugins) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + + return loadOrder; +} diff --git a/libs/game_bethesda/src/games/enderal/enderalgameplugins.h b/libs/game_bethesda/src/games/enderal/enderalgameplugins.h new file mode 100644 index 0000000..f78904d --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalgameplugins.h @@ -0,0 +1,23 @@ +#ifndef _ENDERALGAMEPLUGINS_H +#define _ENDERALGAMEPLUGINS_H + +#include +#include +#include +#include + +class EnderalGamePlugins : public GamebryoGamePlugins +{ +public: + EnderalGamePlugins(MOBase::IOrganizer* organizer); + + virtual void readPluginLists(MOBase::IPluginList* pluginList) override; + +protected: + virtual QStringList readPluginList(MOBase::IPluginList* pluginList) override; + +private: + std::map m_LastSaveHash; +}; + +#endif // _ENDERALSEGAMEPLUGINS_H diff --git a/libs/game_bethesda/src/games/enderal/enderallocalsavegames.cpp b/libs/game_bethesda/src/games/enderal/enderallocalsavegames.cpp new file mode 100644 index 0000000..e01783a --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderallocalsavegames.cpp @@ -0,0 +1,24 @@ +/* +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "enderallocalsavegames.h" + +QString EnderalLocalSavegames::localSavesDummy() const +{ + return "..\\Enderal\\__MO_Saves\\"; +} diff --git a/libs/game_bethesda/src/games/enderal/enderallocalsavegames.h b/libs/game_bethesda/src/games/enderal/enderallocalsavegames.h new file mode 100644 index 0000000..79a8d43 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderallocalsavegames.h @@ -0,0 +1,35 @@ +/* +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef ENDERALLOCALSAVEGAMES_H +#define ENDERALLOCALSAVEGAMES_H + +#include + +#include + +class EnderalLocalSavegames : public GamebryoLocalSavegames +{ +public: + using GamebryoLocalSavegames::GamebryoLocalSavegames; + +protected: + QString localSavesDummy() const override; +}; + +#endif // ENDERALLOCALSAVEGAMES_H diff --git a/libs/game_bethesda/src/games/enderal/enderalmoddatachecker.h b/libs/game_bethesda/src/games/enderal/enderalmoddatachecker.h new file mode 100644 index 0000000..55f8aac --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalmoddatachecker.h @@ -0,0 +1,49 @@ +#ifndef ENDERAL_MODATACHECKER_H +#define ENDERAL_MODATACHECKER_H + +#include + +class EnderalModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{"fonts", + "interface", + "menus", + "meshes", + "music", + "scripts", + "shaders", + "sound", + "strings", + "textures", + "trees", + "video", + "facegen", + "materials", + "skse", + "distantlod", + "asi", + "Tools", + "MCM", + "distantland", + "mits", + "dllplugins", + "SkyProc Patchers", + "CalienteTools", + "NetScriptFramework", + "shadersfx"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "bsa", "modgroups", "ini"}; + return result; + } +}; + +#endif // ENDERAL_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/enderal/enderalmoddatacontent.h b/libs/game_bethesda/src/games/enderal/enderalmoddatacontent.h new file mode 100644 index 0000000..47b45b9 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalmoddatacontent.h @@ -0,0 +1,14 @@ +#ifndef ENDERAL_MODDATACONTENT_H +#define ENDERAL_MODDATACONTENT_H + +#include +#include + +// Like Skyrim, Enderal does not need any change from the default feature: +class EnderalModDataContent : public GamebryoModDataContent +{ +public: + using GamebryoModDataContent::GamebryoModDataContent; +}; + +#endif // ENDERAL_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/enderal/enderalsavegame.cpp b/libs/game_bethesda/src/games/enderal/enderalsavegame.cpp new file mode 100644 index 0000000..cd9beb3 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalsavegame.cpp @@ -0,0 +1,101 @@ +#include "enderalsavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gameenderal.h" + +EnderalSaveGame::EnderalSaveGame(QString const& fileName, GameEnderal const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); + + FILETIME ftime; + fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, ftime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&ftime, &ctime); + setCreationTime(ctime); +} + +void EnderalSaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& saveNumber, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, FILETIME& creationTime) const +{ + file.skip(); // header size + file.skip(); // header version + file.read(saveNumber); + + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + + file.read(playerLocation); + + QString timeOfDay; + file.read(timeOfDay); + + QString race; + file.read(race); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); +} + +std::unique_ptr EnderalSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); + std::unique_ptr fields = std::make_unique(); + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation, + dummyTime); + } + + fields->Screenshot = file.readImage(); + + file.skip(); // form version + file.skip(); // plugin info size + + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/enderal/enderalsavegame.h b/libs/game_bethesda/src/games/enderal/enderalsavegame.h new file mode 100644 index 0000000..c29eb46 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalsavegame.h @@ -0,0 +1,23 @@ +#ifndef ENDERALSAVEGAME_H +#define ENDERALSAVEGAME_H + +#include "gamebryosavegame.h" +#include "windows_compat.h" + +class GameEnderal; + +class EnderalSaveGame : public GamebryoSaveGame +{ +public: + EnderalSaveGame(QString const& fileName, GameEnderal const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& wrapper, uint32_t& saveNumber, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation, FILETIME& creationTime) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // ENDERALSAVEGAME_H diff --git a/libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp b/libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp new file mode 100644 index 0000000..ff3864b --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp @@ -0,0 +1,18 @@ +#include "enderalscriptextender.h" + +#include +#include + +EnderalScriptExtender::EnderalScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString EnderalScriptExtender::BinaryName() const +{ + return "skse_loader.exe"; +} + +QString EnderalScriptExtender::PluginPath() const +{ + return "skse/plugins"; +} diff --git a/libs/game_bethesda/src/games/enderal/enderalscriptextender.h b/libs/game_bethesda/src/games/enderal/enderalscriptextender.h new file mode 100644 index 0000000..7d0d027 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalscriptextender.h @@ -0,0 +1,17 @@ +#ifndef ENDERALSCRIPTEXTENDER_H +#define ENDERALSCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class EnderalScriptExtender : public GamebryoScriptExtender +{ +public: + EnderalScriptExtender(const GameGamebryo* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // ENDERALSCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/enderal/game_enderal_en.ts b/libs/game_bethesda/src/games/enderal/game_enderal_en.ts new file mode 100644 index 0000000..9dcac4d --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/game_enderal_en.ts @@ -0,0 +1,17 @@ + + + + + GameEnderal + + + Enderal Support Plugin + + + + + Adds support for the game Enderal + + + + diff --git a/libs/game_bethesda/src/games/enderal/gameenderal.cpp b/libs/game_bethesda/src/games/enderal/gameenderal.cpp new file mode 100644 index 0000000..fb4e1d7 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/gameenderal.cpp @@ -0,0 +1,267 @@ +#include "gameenderal.h" + +#include "enderalbsainvalidation.h" +#include "enderaldataarchives.h" +#include "enderalgameplugins.h" +#include "enderallocalsavegames.h" +#include "enderalmoddatachecker.h" +#include "enderalmoddatacontent.h" +#include "enderalsavegame.h" +#include "enderalscriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" +#include "steamutility.h" +#include "utility.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#ifdef _WIN32 +#include +#include +#endif + +#include +#include +#include +#include +#include + +using namespace MOBase; + +GameEnderal::GameEnderal() {} + +bool GameEnderal::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "enderal.ini")); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameEnderal::gameName() const +{ + return "Enderal"; +} + +QList GameEnderal::executables() const +{ + return QList() + //<< ExecutableInfo("SKSE", + // findInGameFolder(feature()->loaderName())) + //<< ExecutableInfo("SBW", findInGameFolder("SBW.exe")) + << ExecutableInfo("Enderal (SKSE)", findInGameFolder(binaryName())) + << ExecutableInfo("Enderal Launcher", findInGameFolder(getLauncherName())) + << ExecutableInfo("BOSS", findInGameFolder("BOSS/BOSS.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Enderal\"") + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("202480"); +} + +QList GameEnderal::executableForcedLoads() const +{ + return QList(); +} + +QString GameEnderal::name() const +{ + return "Enderal Support Plugin"; +} + +QString GameEnderal::localizedName() const +{ + return tr("Enderal Support Plugin"); +} + +QString GameEnderal::author() const +{ + return "AL12 & MO2 Team"; +} + +QString GameEnderal::description() const +{ + return tr("Adds support for the game Enderal"); +} + +MOBase::VersionInfo GameEnderal::version() const +{ + return VersionInfo(1, 3, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameEnderal::settings() const +{ + QList results; + results.push_back( + PluginSetting("sse_downloads", "allow Skyrim SE downloads", QVariant(false))); + return results; +} + +void GameEnderal::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/enderal", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/enderal.ini").exists()) { + + // there is no default ini, actually they are going to put them in for us! + copyToProfile(gameDirectory().absolutePath(), path, "enderal_default.ini", + "enderal.ini"); + copyToProfile(gameDirectory().absolutePath(), path, "enderalprefs_default.ini", + "enderalprefs.ini"); + } else { + copyToProfile(myGamesPath(), path, "enderal.ini"); + copyToProfile(myGamesPath(), path, "enderalprefs.ini"); + } + } +} + +bool GameEnderal::looksValid(QDir const& path) const +{ + // we need to check both launcher and binary because the binary also exists for + // Skyrim and the launcher for Enderal SE + return path.exists(getLauncherName()) && path.exists(binaryName()); +} + +QIcon GameEnderal::gameIcon() const +{ + return MOBase::iconForExecutable(gameDirectory().absoluteFilePath(getLauncherName())); +} + +QString GameEnderal::savegameExtension() const +{ + return "ess"; +} + +QString GameEnderal::savegameSEExtension() const +{ + return "skse"; +} + +std::shared_ptr +GameEnderal::makeSaveGame(QString filepath) const +{ + return std::make_shared(filepath, this); +} + +QString GameEnderal::steamAPPId() const +{ + return "933480"; +} + +QStringList GameEnderal::primaryPlugins() const +{ + return {"Skyrim.esm", "Enderal - Forgotten Stories.esm", "Update.esm"}; +} + +QString GameEnderal::binaryName() const +{ + return "skse_loader.exe"; +} + +QString GameEnderal::getLauncherName() const +{ + return "Enderal Launcher.exe"; +} + +QString GameEnderal::gameShortName() const +{ + return "Enderal"; +} + +QString GameEnderal::gameNexusName() const +{ + return "enderal"; +} + +QStringList GameEnderal::primarySources() const +{ + return {"Skyrim"}; +} + +QStringList GameEnderal::validShortNames() const +{ + QStringList results; + results.push_back("Skyrim"); + if (m_Organizer->pluginSetting(name(), "sse_downloads").toBool()) { + results.push_back("SkyrimSE"); + } + return results; +} + +QStringList GameEnderal::iniFiles() const +{ + return {"enderal.ini", "enderalprefs.ini"}; +} + +QStringList GameEnderal::DLCPlugins() const +{ + return {}; +} + +IPluginGame::LoadOrderMechanism GameEnderal::loadOrderMechanism() const +{ + return LoadOrderMechanism::PluginsTxt; +} + +int GameEnderal::nexusModOrganizerID() const +{ + return 0; +} + +int GameEnderal::nexusGameID() const +{ + return 2736; +} + +QString GameEnderal::identifyGamePath() const +{ +#ifdef _WIN32 + QString path = "Software\\SureAI\\Enderal"; + QString result; + try { + result = + findInRegistry(HKEY_CURRENT_USER, path.toStdWString().c_str(), L"Install_Path"); + } catch (MOBase::MyException) { + result = MOBase::findSteamGame("Enderal", "Data\\Enderal - Forgotten Stories.esm"); + } + return result; +#else + // Prefer exact Steam app-id resolution for Enderal LE. + QString result = parseSteamLocation(steamAPPId(), "Enderal"); + if (!result.isEmpty() && looksValid(QDir(result))) { + return result; + } + + result = MOBase::findSteamGame("Enderal", "Data/Enderal - Forgotten Stories.esm"); + if (!result.isEmpty() && looksValid(QDir(result))) { + return result; + } + + return GameGamebryo::identifyGamePath(); +#endif +} diff --git a/libs/game_bethesda/src/games/enderal/gameenderal.h b/libs/game_bethesda/src/games/enderal/gameenderal.h new file mode 100644 index 0000000..fb0a377 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/gameenderal.h @@ -0,0 +1,61 @@ +#ifndef GAMEENDERAL_H +#define GAMEENDERAL_H + +#include "gamegamebryo.h" + +#include +#include + +class GameEnderal : public GameGamebryo +{ + Q_OBJECT +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.GameEnderal" FILE "gameenderal.json") +#endif + +public: + GameEnderal(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QString binaryName() const override; + virtual QString getLauncherName() const override; + virtual QString gameShortName() const override; + virtual QStringList validShortNames() const override; + virtual QString gameNexusName() const override; + virtual QStringList primarySources() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + virtual bool looksValid(QDir const&) const override; + virtual QIcon gameIcon() const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + +protected: + virtual QString savegameExtension() const override; + virtual QString savegameSEExtension() const override; + virtual std::shared_ptr + makeSaveGame(QString filepath) const override; + + virtual QString identifyGamePath() const override; +}; + +#endif // GAMEENDERAL_H diff --git a/libs/game_bethesda/src/games/enderal/gameenderal.json b/libs/game_bethesda/src/games/enderal/gameenderal.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/gameenderal.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/enderalse/CMakeLists.txt b/libs/game_bethesda/src/games/enderalse/CMakeLists.txt new file mode 100644 index 0000000..1e33fbf --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_enderalse SHARED + enderalsedataarchives.cpp + enderalsedataarchives.h + enderalsegameplugins.cpp + enderalsegameplugins.h + enderalselocalsavegames.cpp + enderalselocalsavegames.h + enderalsemoddatachecker.h + enderalsemoddatacontent.h + enderalsesavegame.cpp + enderalsesavegame.h + enderalsescriptextender.cpp + enderalsescriptextender.h + enderalseunmanagedmods.cpp + enderalseunmanagedmods.h + gameenderalse.cpp + gameenderalse.h +) +mo2_configure_plugin(game_enderalse NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_enderalse PRIVATE game_creation) +mo2_install_plugin(game_enderalse) diff --git a/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp b/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp new file mode 100644 index 0000000..606c688 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp @@ -0,0 +1,64 @@ +#include "enderalsedataarchives.h" + +#include "iprofile.h" +#include + +QStringList EnderalSEDataArchives::vanillaArchives() const +{ + return {"Skyrim - Textures0.bsa", + "Skyrim - Textures1.bsa", + "Skyrim - Textures2.bsa", + "Skyrim - Textures3.bsa", + "Skyrim - Textures4.bsa", + "Skyrim - Textures5.bsa", + "Skyrim - Textures6.bsa", + "Skyrim - Textures7.bsa", + "Skyrim - Textures8.bsa", + "Skyrim - Meshes0.bsa", + "Skyrim - Meshes1.bsa", + "Skyrim - Voices_en0.bsa", + "Skyrim - Sounds.bsa", + "Skyrim - Interface.bsa", + "Skyrim - Animations.bsa", + "Skyrim - Shaders.bsa", + "Skyrim - Misc.bsa", + "E - Meshes.bsa", + "E - SE.bsa", + "E - Scripts.bsa", + "E - Sounds.bsa", + "E - Textures1.bsa", + "E - Textures2.bsa", + "E - Textures3.bsa", + "L - Textures.bsa", + "L - Voices.bsa"}; +} + +QStringList EnderalSEDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("enderal.ini") + : localGameDirectory().absoluteFilePath("enderal.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList", 512)); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2", 512)); + + return result; +} + +void EnderalSEDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(","); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("enderal.ini") + : localGameDirectory().absoluteFilePath("enderal.ini"); + if (list.length() > 511) { + int splitIdx = list.lastIndexOf(",", 512); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.h b/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.h new file mode 100644 index 0000000..4c51e93 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.h @@ -0,0 +1,26 @@ +#ifndef ENDERALSEDATAARCHIVES_H +#define ENDERALSEDATAARCHIVES_H + +#include "gamebryodataarchives.h" +#include +#include + +namespace MOBase +{ +class IProfile; +} + +class EnderalSEDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // _SKYRIMSEDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp b/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp new file mode 100644 index 0000000..d7be8ce --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp @@ -0,0 +1,82 @@ +#include "enderalsegameplugins.h" + +#include +#include +#include +#include + +#include + +using namespace MOBase; + +void EnderalSEGamePlugins::writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) +{ + SafeWriteFile file(filePath); + + QStringEncoder encoder(QStringConverter::Encoding::System); + + file->resize(0); + + file->write( + encoder.encode("# This file was automatically generated by Mod Organizer.\r\n")); + + bool invalidFileNames = false; + int writtenCount = 0; + + QStringList plugins = pluginList->pluginNames(); + std::sort(plugins.begin(), plugins.end(), + [pluginList](const QString& lhs, const QString& rhs) { + return pluginList->priority(lhs) < pluginList->priority(rhs); + }); + + QStringList PrimaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList DLCPlugins = organizer()->managedGame()->DLCPlugins(); + QSet ManagedMods = + QSet(PrimaryPlugins.begin(), PrimaryPlugins.end()); + QSet DLCSet = QSet(DLCPlugins.begin(), DLCPlugins.end()); + ManagedMods.subtract(DLCSet); + PrimaryPlugins.append(QList(ManagedMods.begin(), ManagedMods.end())); + + // we need to force some plugins because those are not force-loaded + // by the game but are considered primary plugins for users + file->write("*Enderal - Forgotten Stories.esm\r\n"); + file->write("*SkyUI_SE.esp\r\n"); + + // TODO: do not write plugins in OFFICIAL_FILES container + for (const QString& pluginName : plugins) { + if (!PrimaryPlugins.contains(pluginName, Qt::CaseInsensitive)) { + if (pluginList->state(pluginName) == IPluginList::STATE_ACTIVE) { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write("*"); + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } else { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } + } + } + + if (invalidFileNames) { + reportError(QObject::tr("Some of your plugins have invalid names! These " + "plugins can not be loaded by the game. Please see " + "mo_interface.log for a list of affected plugins " + "and rename them.")); + } + + file->commit(); +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.h b/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.h new file mode 100644 index 0000000..700a074 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.h @@ -0,0 +1,19 @@ +#ifndef ENDERALSEGAMEPLUGINS_H +#define ENDERALSEGAMEPLUGINS_H + +#include +#include +#include +#include + +class EnderalSEGamePlugins : public CreationGamePlugins +{ +public: + using CreationGamePlugins::CreationGamePlugins; + +protected: + void writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) override; +}; + +#endif // ENDERALSEGAMEPLUGINS_H diff --git a/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp b/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp new file mode 100644 index 0000000..fafa739 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp @@ -0,0 +1,24 @@ +/* +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "enderalselocalsavegames.h" + +QString EnderalSELocalSavegames::localSavesDummy() const +{ + return "..\\Enderal Special Edition\\__MO_Saves\\"; +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.h b/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.h new file mode 100644 index 0000000..f0d14ad --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.h @@ -0,0 +1,17 @@ +#ifndef ENDERALSELOCALSAVEGAMES_H +#define ENDERALSELOCALSAVEGAMES_H + +#include + +#include + +class EnderalSELocalSavegames : public GamebryoLocalSavegames +{ +public: + using GamebryoLocalSavegames::GamebryoLocalSavegames; + +protected: + QString localSavesDummy() const override; +}; + +#endif // ENDERALSELOCALSAVEGAMES_H diff --git a/libs/game_bethesda/src/games/enderalse/enderalsemoddatachecker.h b/libs/game_bethesda/src/games/enderalse/enderalsemoddatachecker.h new file mode 100644 index 0000000..2336819 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsemoddatachecker.h @@ -0,0 +1,31 @@ +#ifndef ENDERALSE_MODATACHECKER_H +#define ENDERALSE_MODATACHECKER_H + +#include + +class EnderalSEModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{ + "fonts", "interface", "menus", "meshes", + "music", "scripts", "shaders", "sound", + "strings", "textures", "trees", "video", + "facegen", "materials", "skse", "distantlod", + "asi", "Tools", "MCM", "distantland", + "mits", "dllplugins", "CalienteTools", "NetScriptFramework", + "shadersfx", "Nemesis_Engine"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "esl", "bsa", "modgroups", "ini"}; + return result; + } +}; + +#endif // SKYRIMSE_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/enderalse/enderalsemoddatacontent.h b/libs/game_bethesda/src/games/enderalse/enderalsemoddatacontent.h new file mode 100644 index 0000000..a9cce03 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsemoddatacontent.h @@ -0,0 +1,21 @@ +#ifndef ENDERALSE_MODDATACONTENT_H +#define ENDERALSE_MODDATACONTENT_H + +#include +#include + +class EnderalSEModDataContent : public GamebryoModDataContent +{ +public: + /** + * + */ + EnderalSEModDataContent(MOBase::IGameFeatures* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + // Just need to disable some contents: + m_Enabled[CONTENT_SKYPROC] = false; + } +}; + +#endif // SKYRIMSE_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp b/libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp new file mode 100644 index 0000000..2662505 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp @@ -0,0 +1,148 @@ +#include "enderalsesavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +union _ULARGE_INTEGER { + struct { + uint32_t LowPart; + uint32_t HighPart; + }; + uint64_t QuadPart; +}; + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +EnderalSESaveGame::EnderalSESaveGame(QString const& fileName, GameEnderalSE const* game) + : GamebryoSaveGame(fileName, game, true) +{ + FileWrapper file(fileName, "TESV_SAVEGAME"); // 10bytes + + uint32_t version; + FILETIME ftime; + fetchInformationFields(file, version, m_PCName, m_PCLevel, m_PCLocation, m_SaveNumber, + ftime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + + // For some reason, the file time is off by about 6 hours. + // So we need to subtract those 6 hours from the filetime. + _ULARGE_INTEGER time; + time.LowPart = ftime.dwLowDateTime; + time.HighPart = ftime.dwHighDateTime; + time.QuadPart -= 2.16e11; + ftime.dwHighDateTime = time.HighPart; + ftime.dwLowDateTime = time.LowPart; + + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&ftime, &ctime); + + setCreationTime(ctime); +} + +void EnderalSESaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& version, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, uint32_t& saveNumber, + FILETIME& creationTime) const +{ + uint32_t headerSize; + file.read(headerSize); // header size "TESV_SAVEGAME" + file.read(version); + file.read(saveNumber); + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString timeOfDay; + file.read(timeOfDay); + + QString race; + file.read(race); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); // filetime +} + +std::unique_ptr EnderalSESaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); // 10bytes + + uint32_t version = 0; + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, version, dummyName, dummyLevel, dummyLocation, + dummySaveNumber, dummyTime); + } + + std::unique_ptr fields = std::make_unique(); + + uint32_t width; + uint32_t height; + file.read(width); + file.read(height); + + bool alpha = false; + + // compatibility between LE and SE: + // SE has an additional uin16_t for compression + // SE uses an alpha channel, whereas LE does not + if (version == 12) { + uint16_t compressionType; + file.read(compressionType); + file.setCompressionType(compressionType); + alpha = true; + } + + fields->Screenshot = file.readImage(width, height, 320, alpha); + + file.openCompressedData(); + + uint8_t saveGameVersion = file.readChar(); + uint8_t pluginInfoSize = file.readChar(); + uint16_t other = file.readShort(); // Unknown + + fields->Plugins = file.readPlugins(1); // Just empty data + + if (saveGameVersion >= 78) { + fields->LightPlugins = file.readLightPlugins(); + } + + file.closeCompressedData(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalsesavegame.h b/libs/game_bethesda/src/games/enderalse/enderalsesavegame.h new file mode 100644 index 0000000..1363142 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsesavegame.h @@ -0,0 +1,28 @@ +#ifndef ENDERALSESAVEGAME_H +#define ENDERALSESAVEGAME_H + +#include "gamebryosavegame.h" +#include "gameenderalse.h" +#include "windows_compat.h" + +namespace MOBase +{ +class IPluginGame; +} + +class EnderalSESaveGame : public GamebryoSaveGame +{ +public: + EnderalSESaveGame(QString const& fileName, GameEnderalSE const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& wrapper, uint32_t& version, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation, uint32_t& saveNumber, + FILETIME& creationTime) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // _SKYRIMSESAVEGAME_H diff --git a/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp b/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp new file mode 100644 index 0000000..f7a3113 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp @@ -0,0 +1,18 @@ +#include "enderalsescriptextender.h" + +#include +#include + +EnderalSEScriptExtender::EnderalSEScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString EnderalSEScriptExtender::BinaryName() const +{ + return "skse64_loader.exe"; +} + +QString EnderalSEScriptExtender::PluginPath() const +{ + return "skse/plugins"; +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.h b/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.h new file mode 100644 index 0000000..79edaf2 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.h @@ -0,0 +1,17 @@ +#ifndef ENDERALSESCRIPTEXTENDER_H +#define ENDERALESCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class EnderalSEScriptExtender : public GamebryoScriptExtender +{ +public: + EnderalSEScriptExtender(GameGamebryo const* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // _SKYRIMSESCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp b/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp new file mode 100644 index 0000000..9f11b44 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp @@ -0,0 +1,29 @@ +#include "enderalseunmanagedmods.h" + +EnderalSEUnmangedMods::EnderalSEUnmangedMods(const GameGamebryo* game) + : GamebryoUnmangedMods(game) +{} + +EnderalSEUnmangedMods::~EnderalSEUnmangedMods() {} + +QStringList EnderalSEUnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QDir dataDir(game()->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + + return result; +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.h b/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.h new file mode 100644 index 0000000..6eda502 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.h @@ -0,0 +1,16 @@ +#ifndef ENDERALSEUNMANAGEDMODS_H +#define ENDERALSEUNMANAGEDMODS_H + +#include "gamebryounmanagedmods.h" +#include + +class EnderalSEUnmangedMods : public GamebryoUnmangedMods +{ +public: + EnderalSEUnmangedMods(const GameGamebryo* game); + ~EnderalSEUnmangedMods(); + + virtual QStringList mods(bool onlyOfficial) const override; +}; + +#endif // _SKYRIMSEUNMANAGEDMODS_H diff --git a/libs/game_bethesda/src/games/enderalse/game_enderalse_en.ts b/libs/game_bethesda/src/games/enderalse/game_enderalse_en.ts new file mode 100644 index 0000000..4c8b5a2 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/game_enderalse_en.ts @@ -0,0 +1,25 @@ + + + + + GameEnderalSE + + + Enderal Special Edition Support Plugin + + + + + Adds support for the game Enderal Special Edition. + + + + + QObject + + + Some of your plugins have invalid names! These plugins can not be loaded by the game. Please see mo_interface.log for a list of affected plugins and rename them. + + + + diff --git a/libs/game_bethesda/src/games/enderalse/gameenderalse.cpp b/libs/game_bethesda/src/games/enderalse/gameenderalse.cpp new file mode 100644 index 0000000..26587b8 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/gameenderalse.cpp @@ -0,0 +1,347 @@ +#include "gameenderalse.h" + +#include "enderalsedataarchives.h" +#include "enderalsegameplugins.h" +#include "enderalselocalsavegames.h" +#include "enderalsemoddatachecker.h" +#include "enderalsemoddatacontent.h" +#include "enderalsesavegame.h" +#include "enderalsescriptextender.h" +#include "enderalseunmanagedmods.h" +#include "steamutility.h" + +#include "versioninfo.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "scopeguard.h" +#include + +using namespace MOBase; + +GameEnderalSE::GameEnderalSE() {} + +void GameEnderalSE::setVariant(QString variant) +{ + m_GameVariant = variant; +} + +void GameEnderalSE::checkVariants() +{ + QFileInfo gog_dll(m_GamePath + "\\Galaxy64.dll"); + if (gog_dll.exists()) + setVariant("GOG"); + else + setVariant("Steam"); +} + +QDir GameEnderalSE::documentsDirectory() const +{ + return m_MyGamesPath; +} + +void GameEnderalSE::detectGame() +{ + m_GamePath = identifyGamePath(); + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QString GameEnderalSE::identifyGamePath() const +{ +#ifdef _WIN32 + QMap paths = { + {"Software\\Bethesda Softworks\\" + gameName(), "Installed Path"}, + {"Software\\GOG.com\\Games\\1708684988", "path"}, + }; + QString result; + try { + for (auto& path : paths.toStdMap()) { + result = findInRegistry(HKEY_LOCAL_MACHINE, path.first.toStdWString().c_str(), + path.second.toStdWString().c_str()); + if (!result.isEmpty()) + break; + } + } catch (MOBase::MyException) { + result = MOBase::findSteamGame("Enderal Special Edition", + "Data\\Enderal - Forgotten Stories.esm"); + } + return result; +#else + // Prefer exact Steam app-id resolution for Enderal SE. + QString result = parseSteamLocation("976620", "Enderal Special Edition"); + if (!result.isEmpty() && looksValid(QDir(result))) { + return result; + } + + result = MOBase::findSteamGame("Enderal Special Edition", + "Data/Enderal - Forgotten Stories.esm"); + if (!result.isEmpty() && looksValid(QDir(result))) { + return result; + } + + return GameGamebryo::identifyGamePath(); +#endif +} + +void GameEnderalSE::setGamePath(const QString& path) +{ + m_GamePath = path; + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QDir GameEnderalSE::savesDirectory() const +{ + return QDir(m_MyGamesPath + "/Saves"); +} + +bool GameEnderalSE::isInstalled() const +{ + return !m_GamePath.isEmpty(); +} + +bool GameEnderalSE::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(this, "enderal.ini")); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo->gameFeatures())); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameEnderalSE::gameName() const +{ + return "Enderal Special Edition"; +} + +QString GameEnderalSE::gameDirectoryName() const +{ + if (selectedVariant() == "GOG") + return "Enderal Special Edition GOG"; + else + return "Enderal Special Edition"; +} + +QIcon GameEnderalSE::gameIcon() const +{ + return MOBase::iconForExecutable(gameDirectory().absoluteFilePath(getLauncherName())); +} + +QList GameEnderalSE::executables() const +{ + return QList() + << ExecutableInfo("Enderal Special Edition (SKSE)", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Enderal Special Edition Launcher", + findInGameFolder(getLauncherName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("1946180") + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Enderal Special Edition\""); +} + +QList GameEnderalSE::executableForcedLoads() const +{ + return QList(); +} + +QString GameEnderalSE::binaryName() const +{ + return "skse64_loader.exe"; +} + +QString GameEnderalSE::getLauncherName() const +{ + return "Enderal Launcher.exe"; +} + +bool GameEnderalSE::looksValid(const QDir& folder) const +{ + // we need to check both launcher and binary because the binary also exists for + // Skyrim SE and the launcher for Enderal LE + return folder.exists(getLauncherName()) && folder.exists(binaryName()); +} + +QFileInfo GameEnderalSE::findInGameFolder(const QString& relativePath) const +{ + return QFileInfo(m_GamePath + "/" + relativePath); +} + +QString GameEnderalSE::name() const +{ + return "Enderal Special Edition Support Plugin"; +} + +QString GameEnderalSE::localizedName() const +{ + return tr("Enderal Special Edition Support Plugin"); +} + +QString GameEnderalSE::author() const +{ + return "Archost, ZachHaber & MO2 Team"; +} + +QString GameEnderalSE::description() const +{ + return tr("Adds support for the game Enderal Special Edition."); +} + +MOBase::VersionInfo GameEnderalSE::version() const +{ + return VersionInfo(1, 2, 0, VersionInfo::RELEASE_FINAL); +} + +QList GameEnderalSE::settings() const +{ + return QList(); +} + +void GameEnderalSE::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + gameDirectoryName(), path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/Enderal.ini").exists()) { + + // there is no default ini, actually they are going to put them in for us! + copyToProfile(gameDirectory().absolutePath(), path, "enderal_default.ini", + "Enderal.ini"); + copyToProfile(gameDirectory().absolutePath(), path, "enderalprefs_default.ini", + "EnderalPrefs.ini"); + } else { + copyToProfile(myGamesPath(), path, "Enderal.ini"); + copyToProfile(myGamesPath(), path, "EnderalPrefs.ini"); + } + } +} + +QString GameEnderalSE::savegameExtension() const +{ + return "ess"; +} + +QString GameEnderalSE::savegameSEExtension() const +{ + return "skse"; +} + +std::shared_ptr +GameEnderalSE::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameEnderalSE::steamAPPId() const +{ + if (selectedVariant() == "Steam") + return "976620"; + return ""; +} + +QStringList GameEnderalSE::primaryPlugins() const +{ + return {"skyrim.esm", "update.esm", "dawnguard.esm", "hearthfires.esm", + "dragonborn.esm", + + // these two plugins are considered "primary" for users but are not + // automatically loaded by the game so we need to force-write them + // to the plugin list + "enderal - forgotten stories.esm", "skyui_se.esp"}; +} + +QStringList GameEnderalSE::DLCPlugins() const +{ + return {}; +} + +QStringList GameEnderalSE::gameVariants() const +{ + return {"Steam", "GOG"}; +} + +QString GameEnderalSE::gameShortName() const +{ + return "EnderalSE"; +} + +QStringList GameEnderalSE::validShortNames() const +{ + return {"Skyrim", "SkyrimSE", "Enderal"}; +} + +QString GameEnderalSE::gameNexusName() const +{ + return "enderalspecialedition"; +} + +QStringList GameEnderalSE::iniFiles() const +{ + return {"Enderal.ini", "EnderalPrefs.ini"}; +} + +QStringList GameEnderalSE::CCPlugins() const +{ + return {}; +} + +IPluginGame::LoadOrderMechanism GameEnderalSE::loadOrderMechanism() const +{ + return IPluginGame::LoadOrderMechanism::PluginsTxt; +} + +int GameEnderalSE::nexusModOrganizerID() const +{ + return 0; +} + +int GameEnderalSE::nexusGameID() const +{ + return 3685; +} + +QDir GameEnderalSE::gameDirectory() const +{ + return QDir(m_GamePath); +} + +// Not to delete all the spaces... +MappingType GameEnderalSE::mappings() const +{ + MappingType result; + + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameDirectoryName() + "/" + profileFile, + false}); + } + + return result; +} diff --git a/libs/game_bethesda/src/games/enderalse/gameenderalse.h b/libs/game_bethesda/src/games/enderalse/gameenderalse.h new file mode 100644 index 0000000..754951d --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/gameenderalse.h @@ -0,0 +1,75 @@ +#ifndef GAMEENDERALSE_H +#define GAMEENDERALSE_H + +#include "gamegamebryo.h" + +#include +#include + +class GameEnderalSE : public GameGamebryo +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "com.soundcontactstudio.GameEnderalSE" FILE + "gameenderalse.json") + +public: + GameEnderalSE(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual void detectGame() override; + virtual QString gameName() const override; + virtual QIcon gameIcon() const override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QStringList gameVariants() const override; + virtual QString binaryName() const override; + virtual QString getLauncherName() const override; + virtual bool looksValid(const QDir& folder) const override; + virtual QString gameShortName() const override; + virtual QString gameNexusName() const override; + virtual QStringList validShortNames() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual QStringList CCPlugins() const override; + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + + virtual bool isInstalled() const override; + virtual void setGamePath(const QString& path) override; + virtual QDir gameDirectory() const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + virtual MappingType mappings() const override; + +protected: + std::shared_ptr makeSaveGame(QString filePath) const override; + QString savegameExtension() const override; + QString savegameSEExtension() const override; + + QString gameDirectoryName() const; + QDir documentsDirectory() const; + QDir savesDirectory() const; + QFileInfo findInGameFolder(const QString& relativePath) const; + + void checkVariants(); + void setVariant(QString variant); + + virtual QString identifyGamePath() const override; +}; + +#endif // _GAMESKYRIMSE_H diff --git a/libs/game_bethesda/src/games/enderalse/gameenderalse.json b/libs/game_bethesda/src/games/enderalse/gameenderalse.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/gameenderalse.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/fallout3/CMakeLists.txt b/libs/game_bethesda/src/games/fallout3/CMakeLists.txt new file mode 100644 index 0000000..3db9758 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_fallout3 SHARED + fallout3bsainvalidation.cpp + fallout3bsainvalidation.h + fallout3dataarchives.cpp + fallout3dataarchives.h + fallout3moddatachecker.h + fallout3moddatacontent.h + fallout3savegame.cpp + fallout3savegame.h + fallout3scriptextender.cpp + fallout3scriptextender.h + gamefallout3.cpp + gamefallout3.h +) +mo2_configure_plugin(game_fallout3 NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_fallout3 PRIVATE game_gamebryo) +mo2_install_plugin(game_fallout3) diff --git a/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp b/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp new file mode 100644 index 0000000..ce981fb --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "fallout3bsainvalidation.h" + +Fallout3BSAInvalidation::Fallout3BSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "fallout.ini", game) +{} + +QString Fallout3BSAInvalidation::invalidationBSAName() const +{ + return "Fallout - Invalidation.bsa"; +} + +unsigned long Fallout3BSAInvalidation::bsaVersion() const +{ + return 0x68; +} diff --git a/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.h b/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.h new file mode 100644 index 0000000..f836073 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.h @@ -0,0 +1,20 @@ +#ifndef FALLOUT3BSAINVALIDATION_H +#define FALLOUT3BSAINVALIDATION_H + +#include "fallout3dataarchives.h" +#include "gamebryobsainvalidation.h" + +#include + +class Fallout3BSAInvalidation : public GamebryoBSAInvalidation +{ +public: + Fallout3BSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; +}; + +#endif // FALLOUT3BSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp b/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp new file mode 100644 index 0000000..ed6627b --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp @@ -0,0 +1,33 @@ +#include "fallout3dataarchives.h" + +#include "iprofile.h" +#include + +QStringList Fallout3DataArchives::vanillaArchives() const +{ + return {"Fallout - Textures.bsa", "Fallout - Meshes.bsa", "Fallout - Voices.bsa", + "Fallout - Sound.bsa", "Fallout - MenuVoices.bsa", "Fallout - Misc.bsa"}; +} + +QStringList Fallout3DataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini") + : localGameDirectory().absoluteFilePath("fallout.ini"); + result.append(getArchivesFromKey(iniFile, "SArchiveList")); + + return result; +} + +void Fallout3DataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini") + : localGameDirectory().absoluteFilePath("fallout.ini"); + setArchivesToKey(iniFile, "SArchiveList", list); +} diff --git a/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.h b/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.h new file mode 100644 index 0000000..8ba4561 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.h @@ -0,0 +1,20 @@ +#ifndef FALLOUT3DATAARCHIVES_H +#define FALLOUT3DATAARCHIVES_H + +#include "gamebryodataarchives.h" +#include + +class Fallout3DataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // FALLOUT3DATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/fallout3/fallout3moddatachecker.h b/libs/game_bethesda/src/games/fallout3/fallout3moddatachecker.h new file mode 100644 index 0000000..0051137 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3moddatachecker.h @@ -0,0 +1,31 @@ +#ifndef FALLOUT3_MODATACHECKER_H +#define FALLOUT3_MODATACHECKER_H + +#include + +class Fallout3ModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{ + "fonts", "interface", "menus", "meshes", + "music", "scripts", "shaders", "sound", + "strings", "textures", "trees", "video", + "facegen", "materials", "fose", "distantlod", + "asi", "Tools", "MCM", "distantland", + "mits", "dllplugins", "CalienteTools", "NetScriptFramework", + "shadersfx"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "bsa", "modgroups", "ini"}; + return result; + } +}; + +#endif // FALLOUT3_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/fallout3/fallout3moddatacontent.h b/libs/game_bethesda/src/games/fallout3/fallout3moddatacontent.h new file mode 100644 index 0000000..bc314a9 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3moddatacontent.h @@ -0,0 +1,22 @@ +#ifndef FALLOUT3_MODDATACONTENT_H +#define FALLOUT3_MODDATACONTENT_H + +#include +#include + +class Fallout3ModDataContent : public GamebryoModDataContent +{ +public: + /** + * + */ + Fallout3ModDataContent(MOBase::IGameFeatures const* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + // Just need to disable some contents: + m_Enabled[CONTENT_MCM] = false; + m_Enabled[CONTENT_SKYPROC] = false; + } +}; + +#endif // FALLOUT3_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/fallout3/fallout3savegame.cpp b/libs/game_bethesda/src/games/fallout3/fallout3savegame.cpp new file mode 100644 index 0000000..aa9bcde --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3savegame.cpp @@ -0,0 +1,72 @@ +#include "fallout3savegame.h" + +#include "gamefallout3.h" + +Fallout3SaveGame::Fallout3SaveGame(QString const& fileName, GameFallout3 const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "FO3SAVEGAME"); + + uint32_t width, height; + fetchInformationFields(file, width, height, m_SaveNumber, m_PCName, m_PCLevel, + m_PCLocation); +} + +void Fallout3SaveGame::fetchInformationFields(FileWrapper& file, uint32_t& width, + uint32_t& height, + uint32_t& saveNumber, + QString& playerName, + unsigned short& playerLevel, + QString& playerLocation) const +{ + file.skip(); // Save header size + + file.setHasFieldMarkers(true); + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BZSTRING); + + file.skip(); // File version ? + file.skip(); // delimiter + + file.read(width); + file.read(height); + file.read(saveNumber); + file.read(playerName); + + QString whatthis; + file.read(whatthis); + + long level; + file.read(level); + playerLevel = level; + + file.read(playerLocation); +} + +std::unique_ptr Fallout3SaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "FO3SAVEGAME"); + + std::unique_ptr fields = std::make_unique(); + + uint32_t width, height; + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + + fetchInformationFields(file, width, height, dummySaveNumber, dummyName, dummyLevel, + dummyLocation); + } + + QString playtime; + file.read(playtime); + + fields->Screenshot = file.readImage(width, height, 256); + + file.skip(5); // unknown (1 byte), plugin size (4 bytes) + + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING); + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/fallout3/fallout3savegame.h b/libs/game_bethesda/src/games/fallout3/fallout3savegame.h new file mode 100644 index 0000000..2a33037 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3savegame.h @@ -0,0 +1,23 @@ +#ifndef FALLOUT3SAVEGAME_H +#define FALLOUT3SAVEGAME_H + +#include "gamebryosavegame.h" + +class GameFallout3; + +class Fallout3SaveGame : public GamebryoSaveGame +{ +public: + Fallout3SaveGame(QString const& fileName, GameFallout3 const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& wrapper, uint32_t& width, + uint32_t& height, uint32_t& saveNumber, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // FALLOUT3SAVEGAME_H diff --git a/libs/game_bethesda/src/games/fallout3/fallout3scriptextender.cpp b/libs/game_bethesda/src/games/fallout3/fallout3scriptextender.cpp new file mode 100644 index 0000000..608a305 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3scriptextender.cpp @@ -0,0 +1,18 @@ +#include "fallout3scriptextender.h" + +#include +#include + +Fallout3ScriptExtender::Fallout3ScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString Fallout3ScriptExtender::BinaryName() const +{ + return "fose_loader.exe"; +} + +QString Fallout3ScriptExtender::PluginPath() const +{ + return "fose/plugins"; +} diff --git a/libs/game_bethesda/src/games/fallout3/fallout3scriptextender.h b/libs/game_bethesda/src/games/fallout3/fallout3scriptextender.h new file mode 100644 index 0000000..1769d6b --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3scriptextender.h @@ -0,0 +1,17 @@ +#ifndef FALLOUT3SCRIPTEXTENDER_H +#define FALLOUT3SCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class Fallout3ScriptExtender : public GamebryoScriptExtender +{ +public: + Fallout3ScriptExtender(GameGamebryo const* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // FALLOUT3SCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/fallout3/game_fallout3_en.ts b/libs/game_bethesda/src/games/fallout3/game_fallout3_en.ts new file mode 100644 index 0000000..5b90534 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/game_fallout3_en.ts @@ -0,0 +1,17 @@ + + + + + GameFallout3 + + + Fallout 3 Support Plugin + + + + + Adds support for the game Fallout 3. + + + + diff --git a/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp b/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp new file mode 100644 index 0000000..f2f4562 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp @@ -0,0 +1,279 @@ +#include "gamefallout3.h" + +#include "fallout3bsainvalidation.h" +#include "fallout3dataarchives.h" +#include "fallout3moddatachecker.h" +#include "fallout3moddatacontent.h" +#include "fallout3savegame.h" +#include "fallout3scriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" +#include "versioninfo.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace MOBase; + +GameFallout3::GameFallout3() {} + +bool GameFallout3::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "fallout.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + return true; +} + +QString GameFallout3::identifyVariant() const +{ + if (QFile::exists(m_GamePath + "/Fallout3ng.exe")) { + return "Low Violence"; + } else if (QFile::exists(m_GamePath + "/Galaxy.dll")) { + return "GOG"; + } else if (QFile::exists(m_GamePath + "/FalloutLauncherEpic.exe")) { + return "Epic Games"; + } else if (m_GamePath.endsWith("Fallout 3 goty")) { + return "Steam (Game of the Year)"; + } else { + return "Steam (Regular)"; + } +} + +QString GameFallout3::identifyGamePath() const +{ + // Steam (Regular) + auto result = parseSteamLocation("22300", "Fallout 3"); + + // Steam (Game of the Year) + if (result.isEmpty()) { + result = parseSteamLocation("22370", "Fallout 3 goty"); + } + + // Epic Games + if (result.isEmpty()) { + // Fallout 3: Game of the Year Edition: adeae8bbfc94427db57c7dfecce3f1d4 + result = parseEpicGamesLocation({"adeae8bbfc94427db57c7dfecce3f1d4"}); + if (QFileInfo(result).isDir()) { + QDir startPath = QDir(result); + auto subDirs = + startPath.entryList({"Fallout 3 GOTY*"}, QDir::Dirs | QDir::NoDotAndDotDot); + if (!subDirs.isEmpty()) + result = startPath.absoluteFilePath(subDirs.first()); + } + } + + // GOG (and Steam) + if (result.isEmpty()) { + result = GameGamebryo::identifyGamePath(); + } + + return result; +} + +QString GameFallout3::gameName() const +{ + return "Fallout 3"; +} + +void GameFallout3::detectGame() +{ + m_GamePath = identifyGamePath(); + setGameVariant(identifyVariant()); + m_MyGamesPath = determineMyGamesPath("Fallout3"); +} + +QList GameFallout3::executables() const +{ + return QList() + << ExecutableInfo("FOSE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Fallout 3", findInGameFolder(binaryName())) + << ExecutableInfo("Fallout Mod Manager", findInGameFolder("fomm/fomm.exe")) + << ExecutableInfo("Construction Kit", findInGameFolder("geck.exe")) + << ExecutableInfo("Fallout Launcher", findInGameFolder(getLauncherName())) + << ExecutableInfo("BOSS", findInGameFolder("BOSS/BOSS.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Fallout3\""); +} + +QList GameFallout3::executableForcedLoads() const +{ + return QList(); +} + +QString GameFallout3::name() const +{ + return "Fallout 3 Support Plugin"; +} + +QString GameFallout3::localizedName() const +{ + return tr("Fallout 3 Support Plugin"); +} + +QString GameFallout3::author() const +{ + return "Tannin"; +} + +QString GameFallout3::description() const +{ + return tr("Adds support for the game Fallout 3."); +} + +MOBase::VersionInfo GameFallout3::version() const +{ + return VersionInfo(1, 4, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameFallout3::settings() const +{ + return QList(); +} + +void GameFallout3::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Fallout3", path, "plugins.txt"); + copyToProfile(localAppFolder() + "/Fallout3", path, "loadorder.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath(), "fallout.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "fallout_default.ini", + "fallout.ini"); + } else { + copyToProfile(myGamesPath(), path, "fallout.ini"); + } + + copyToProfile(myGamesPath(), path, "falloutprefs.ini"); + copyToProfile(myGamesPath(), path, "FalloutCustom.ini"); + copyToProfile(myGamesPath(), path, "custom.ini"); + copyToProfile(myGamesPath(), path, "GECKCustom.ini"); + copyToProfile(myGamesPath(), path, "GECKPrefs.ini"); + } +} + +QString GameFallout3::savegameExtension() const +{ + return "fos"; +} + +QString GameFallout3::savegameSEExtension() const +{ + return ""; +} + +std::shared_ptr +GameFallout3::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameFallout3::steamAPPId() const +{ + if (selectedVariant() == "Steam (Game Of The Year)") { + return "22370"; + } else if (selectedVariant() == "Steam (Regular)") { + return "22300"; + } else { + return ""; + } +} + +QStringList GameFallout3::primaryPlugins() const +{ + return {"fallout3.esm"}; +} + +QStringList GameFallout3::gameVariants() const +{ + return {"Steam (Regular)", "Steam (Game Of The Year)", "Epic Games", "GOG", + "Low Violence"}; +} + +QString GameFallout3::binaryName() const +{ + if (selectedVariant() == "Low Violence") { + return "Fallout3ng.exe"; + } else { + return GameGamebryo::binaryName(); + } +} + +QString GameFallout3::gameShortName() const +{ + return "Fallout3"; +} + +QStringList GameFallout3::validShortNames() const +{ + return {"FalloutNV"}; +} + +QString GameFallout3::gameNexusName() const +{ + return "fallout3"; +} + +QStringList GameFallout3::iniFiles() const +{ + return {"fallout.ini", "falloutprefs.ini", "FalloutCustom.ini", "GECKCustom.ini", + "GECKPrefs.ini"}; +} + +QStringList GameFallout3::DLCPlugins() const +{ + return {"ThePitt.esm", "Anchorage.esm", "BrokenSteel.esm", "PointLookout.esm", + "Zeta.esm"}; +} + +int GameFallout3::nexusModOrganizerID() const +{ + return 16348; +} + +int GameFallout3::nexusGameID() const +{ + return 120; +} + +QString GameFallout3::getLauncherName() const +{ + const QMap names = { + {"Steam (Regular)", "Fallout3Launcher.exe"}, + {"Steam (Game of the Year)", "Fallout3Launcher.exe"}, + {"Epic Games", "FalloutLauncherEpic.exe"}, + {"GOG", "FalloutLauncher.exe"}, + {"Low Violence", "Fallout3Launcher.exe"}}; + + return names.value(selectedVariant()); +} diff --git a/libs/game_bethesda/src/games/fallout3/gamefallout3.h b/libs/game_bethesda/src/games/fallout3/gamefallout3.h new file mode 100644 index 0000000..a6dce27 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/gamefallout3.h @@ -0,0 +1,58 @@ +#ifndef GAMEFALLOUT3_H +#define GAMEFALLOUT3_H + +#include "gamegamebryo.h" + +#include +#include + +class GameFallout3 : public GameGamebryo +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.tannin.GameFallout3" FILE "gamefallout3.json") + +public: + GameFallout3(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual void detectGame() override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QStringList gameVariants() const; + QString binaryName() const override; + virtual QString gameShortName() const override; + virtual QStringList validShortNames() const override; + virtual QString gameNexusName() const override; + virtual QString getLauncherName() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + +protected: + virtual QString identifyGamePath() const override; + virtual QString savegameExtension() const override; + virtual QString savegameSEExtension() const override; + std::shared_ptr makeSaveGame(QString filePath) const override; + +private: + QString identifyVariant() const; +}; + +#endif // GAMEFALLOUT3_H diff --git a/libs/game_bethesda/src/games/fallout3/gamefallout3.json b/libs/game_bethesda/src/games/fallout3/gamefallout3.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/gamefallout3.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/fallout4/CMakeLists.txt b/libs/game_bethesda/src/games/fallout4/CMakeLists.txt new file mode 100644 index 0000000..da33980 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_fallout4 SHARED + fallout4bsainvalidation.cpp + fallout4bsainvalidation.h + fallout4dataarchives.cpp + fallout4dataarchives.h + fallout4moddatachecker.h + fallout4moddatacontent.h + fallout4savegame.cpp + fallout4savegame.h + fallout4scriptextender.cpp + fallout4scriptextender.h + fallout4unmanagedmods.cpp + fallout4unmanagedmods.h + gamefallout4.cpp + gamefallout4.h +) +mo2_configure_plugin(game_fallout4 NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_fallout4 PRIVATE game_creation) +mo2_install_plugin(game_fallout4) diff --git a/libs/game_bethesda/src/games/fallout4/fallout4.qrc b/libs/game_bethesda/src/games/fallout4/fallout4.qrc new file mode 100644 index 0000000..c8e5214 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4.qrc @@ -0,0 +1,5 @@ + + + splash.png + + diff --git a/libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.cpp b/libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.cpp new file mode 100644 index 0000000..7711522 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.cpp @@ -0,0 +1,68 @@ +#include "fallout4bsainvalidation.h" + +#include "dummybsa.h" +#include "iplugingame.h" +#include "iprofile.h" +#include "registry.h" +#include +#include + +#include "gamegamebryo.h" + +Fallout4BSAInvalidation::Fallout4BSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "Fallout4Custom.ini", game) +{ + m_IniFileName = "Fallout4Custom.ini"; + m_Game = game; +} + +bool Fallout4BSAInvalidation::isInvalidationBSA(const QString& bsaName) +{ + return false; +} + +QString Fallout4BSAInvalidation::invalidationBSAName() const +{ + return ""; +} + +unsigned long Fallout4BSAInvalidation::bsaVersion() const +{ + return 0x68; +} + +bool Fallout4BSAInvalidation::prepareProfile(MOBase::IProfile* profile) +{ + bool dirty = false; + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : m_Game->documentsDirectory().absolutePath(); + QString iniFilePath = basePath + "/" + m_IniFileName; + + if (profile->invalidationActive(nullptr)) { + // write bInvalidateOlderFiles = 1, if needed + QString bInvalidateOlderFiles = GameGamebryo::readIniValue( + iniFilePath, "Archive", "bInvalidateOlderFiles", "0"); + if (bInvalidateOlderFiles.toLong() != 1) { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "bInvalidateOlderFiles", "1", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + QString sResourceDataDirsFinal = GameGamebryo::readIniValue( + iniFilePath, "Archive", "sResourceDataDirsFinal", "STRINGS\\"); + if (sResourceDataDirsFinal != "") { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "sResourceDataDirsFinal", "", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } + + return dirty; +} diff --git a/libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.h b/libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.h new file mode 100644 index 0000000..fe52505 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.h @@ -0,0 +1,33 @@ +#ifndef FALLOUT4BSAINVALIDATION_H +#define FALLOUT4BSAINVALIDATION_H + +#include "fallout4dataarchives.h" +#include "gamebryobsainvalidation.h" +#include +#include + +#include + +namespace MOBase +{ +class IPluginGame; +} + +class Fallout4BSAInvalidation : public GamebryoBSAInvalidation +{ +public: + Fallout4BSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + virtual bool isInvalidationBSA(const QString& bsaName) override; + virtual bool prepareProfile(MOBase::IProfile* profile) override; + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; + +private: + QString m_IniFileName; + MOBase::IPluginGame const* m_Game; +}; + +#endif // FALLOUT4BSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp b/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp new file mode 100644 index 0000000..c0931ce --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp @@ -0,0 +1,48 @@ +#include "fallout4dataarchives.h" + +#include "iprofile.h" +#include + +QStringList Fallout4DataArchives::vanillaArchives() const +{ + return {"Fallout4 - Textures1.ba2", "Fallout4 - Textures2.ba2", + "Fallout4 - Textures3.ba2", "Fallout4 - Textures4.ba2", + "Fallout4 - Textures5.ba2", "Fallout4 - Textures6.ba2", + "Fallout4 - Textures7.ba2", "Fallout4 - Textures8.ba2", + "Fallout4 - Textures9.ba2", "Fallout4 - Meshes.ba2", + "Fallout4 - MeshesExtra.ba2", "Fallout4 - Voices.ba2", + "Fallout4 - Sounds.ba2", "Fallout4 - Interface.ba2", + "Fallout4 - Animations.ba2", "Fallout4 - Materials.ba2", + "Fallout4 - Shaders.ba2", "Fallout4 - Startup.ba2", + "Fallout4 - Misc.ba2"}; +} + +QStringList Fallout4DataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini") + : localGameDirectory().absoluteFilePath("fallout4.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + + return result; +} + +void Fallout4DataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini") + : localGameDirectory().absoluteFilePath("fallout4.ini"); + if (list.length() > 255) { + int splitIdx = list.lastIndexOf(",", 256); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.h b/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.h new file mode 100644 index 0000000..c90711c --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.h @@ -0,0 +1,27 @@ +#ifndef FALLOUT4DATAARCHIVES_H +#define FALLOUT4DATAARCHIVES_H + +#include "gamebryodataarchives.h" + +namespace MOBase +{ +class IProfile; +} + +#include +#include + +class Fallout4DataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // FALLOUT4DATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/fallout4/fallout4moddatachecker.h b/libs/game_bethesda/src/games/fallout4/fallout4moddatachecker.h new file mode 100644 index 0000000..08b0497 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4moddatachecker.h @@ -0,0 +1,29 @@ +#ifndef FALLOUT4_MODATACHECKER_H +#define FALLOUT4_MODATACHECKER_H + +#include + +class Fallout4ModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{ + "interface", "meshes", "music", "scripts", "sound", "strings", + "textures", "trees", "video", "materials", "f4se", "distantlod", + "asi", "Tools", "MCM", "distantland", "mits", "dllplugins", + "CalienteTools", "shadersfx", "aaf"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "esl", "ba2", + "modgroups", "ini", "csg", "cdx"}; + return result; + } +}; + +#endif // FALLOUT4_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/fallout4/fallout4moddatacontent.h b/libs/game_bethesda/src/games/fallout4/fallout4moddatacontent.h new file mode 100644 index 0000000..172e94f --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4moddatacontent.h @@ -0,0 +1,44 @@ +#ifndef FALLOUT4_MODDATACONTENT_H +#define FALLOUT4_MODDATACONTENT_H + +#include +#include + +class Fallout4ModDataContent : public GamebryoModDataContent +{ +protected: + enum Fallout4Content + { + CONTENT_MATERIAL = CONTENT_NEXT_VALUE + }; + +public: + Fallout4ModDataContent(const MOBase::IGameFeatures* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + m_Enabled[CONTENT_SKYPROC] = false; + } + + std::vector getAllContents() const override + { + auto contents = GamebryoModDataContent::getAllContents(); + contents.push_back( + Content(CONTENT_MATERIAL, "Materials", ":/MO/gui/content/material")); + return contents; + } + + std::vector + getContentsFor(std::shared_ptr fileTree) const override + { + auto contents = GamebryoModDataContent::getContentsFor(fileTree); + for (auto e : *fileTree) { + if (e->compare("materials") == 0) { + contents.push_back(CONTENT_MATERIAL); + break; // Early break if you have nothing else to check. + } + } + return contents; + } +}; + +#endif // FALLOUT4_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/fallout4/fallout4savegame.cpp b/libs/game_bethesda/src/games/fallout4/fallout4savegame.cpp new file mode 100644 index 0000000..2f88d1b --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4savegame.cpp @@ -0,0 +1,106 @@ +#include "fallout4savegame.h" + +#ifdef _WIN32 +#include +#else +#include + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gamefallout4.h" + +Fallout4SaveGame::Fallout4SaveGame(QString const& fileName, GameFallout4 const* game) + : GamebryoSaveGame(fileName, game, true) +{ + FileWrapper file(getFilepath(), "FO4_SAVEGAME"); + + FILETIME creationTime; + fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, + creationTime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&creationTime, &ctime); + + setCreationTime(ctime); +} + +void Fallout4SaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& saveNumber, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, FILETIME& creationTime) const +{ + file.skip(); // header size + file.skip(); // header version + file.read(saveNumber); + + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString ignore; + file.read(ignore); // playtime as ascii hh.mm.ss + file.read(ignore); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); +} + +std::unique_ptr Fallout4SaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "FO4_SAVEGAME"); // 10bytes + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation, + dummyTime); + } + + QString ignore; + std::unique_ptr fields = std::make_unique(); + + fields->Screenshot = file.readImage(384, true); + + uint8_t saveGameVersion = file.readChar(); + file.read(ignore); // game version + file.skip(); // plugin info size + + fields->Plugins = file.readPlugins(); + if (saveGameVersion >= 68) { + fields->LightPlugins = file.readLightPlugins(); + } + + return fields; +} diff --git a/libs/game_bethesda/src/games/fallout4/fallout4savegame.h b/libs/game_bethesda/src/games/fallout4/fallout4savegame.h new file mode 100644 index 0000000..b50c501 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4savegame.h @@ -0,0 +1,23 @@ +#ifndef FALLOUT4SAVEGAME_H +#define FALLOUT4SAVEGAME_H + +#include "gamebryosavegame.h" +#include "windows_compat.h" + +class GameFallout4; + +class Fallout4SaveGame : public GamebryoSaveGame +{ +public: + Fallout4SaveGame(QString const& fileName, GameFallout4 const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& file, uint32_t& saveNumber, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation, FILETIME& creationTime) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // FALLOUT4SAVEGAME_H diff --git a/libs/game_bethesda/src/games/fallout4/fallout4scriptextender.cpp b/libs/game_bethesda/src/games/fallout4/fallout4scriptextender.cpp new file mode 100644 index 0000000..1207d5e --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4scriptextender.cpp @@ -0,0 +1,18 @@ +#include "fallout4scriptextender.h" + +#include +#include + +Fallout4ScriptExtender::Fallout4ScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString Fallout4ScriptExtender::BinaryName() const +{ + return "f4se_loader.exe"; +} + +QString Fallout4ScriptExtender::PluginPath() const +{ + return "f4se/plugins"; +} diff --git a/libs/game_bethesda/src/games/fallout4/fallout4scriptextender.h b/libs/game_bethesda/src/games/fallout4/fallout4scriptextender.h new file mode 100644 index 0000000..b29df05 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4scriptextender.h @@ -0,0 +1,17 @@ +#ifndef FALLOUT4SCRIPTEXTENDER_H +#define FALLOUT4SCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class Fallout4ScriptExtender : public GamebryoScriptExtender +{ +public: + Fallout4ScriptExtender(GameGamebryo const* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // FALLOUT4SCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.cpp b/libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.cpp new file mode 100644 index 0000000..61b7b2a --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.cpp @@ -0,0 +1,63 @@ +#include "fallout4unmanagedmods.h" + +Fallout4UnmangedMods::Fallout4UnmangedMods(const GameGamebryo* game) + : GamebryoUnmangedMods(game) +{} + +Fallout4UnmangedMods::~Fallout4UnmangedMods() {} + +QStringList Fallout4UnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QDir dataDir(game()->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + + return result; +} + +QStringList Fallout4UnmangedMods::secondaryFiles(const QString& modName) const +{ + // file extension in FO4 is .ba2 instead of bsa + QStringList archives; + QDir dataDir = game()->dataDirectory(); + for (const QString& archiveName : dataDir.entryList({modName + "*.ba2"})) { + archives.append(dataDir.absoluteFilePath(archiveName)); + } + return archives; +} + +QString Fallout4UnmangedMods::displayName(const QString& modName) const +{ + // unlike in earlier games, in fallout 4 the file name doesn't correspond to + // the public name + if (modName.compare("dlcrobot", Qt::CaseInsensitive) == 0) { + return "Automatron"; + } else if (modName.compare("dlcworkshop01", Qt::CaseInsensitive) == 0) { + return "Wasteland Workshop"; + } else if (modName.compare("dlccoast", Qt::CaseInsensitive) == 0) { + return "Far Harbor"; + } else if (modName.compare("dlcworkshop02", Qt::CaseInsensitive) == 0) { + return "Contraptions Workshop"; + } else if (modName.compare("dlcworkshop03", Qt::CaseInsensitive) == 0) { + return "Vault-Tec Workshop"; + } else if (modName.compare("dlcnukaworld", Qt::CaseInsensitive) == 0) { + return "Nuka-World"; + } else if (modName.compare("dlcultrahighresolution", Qt::CaseInsensitive) == 0) { + return "Ultra High Resolution Texture Pack"; + } else { + return modName; + } +} diff --git a/libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.h b/libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.h new file mode 100644 index 0000000..6078b73 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.h @@ -0,0 +1,18 @@ +#ifndef FALLOUT4UNMANAGEDMODS_H +#define FALLOUT4UNMANAGEDMODS_H + +#include "gamebryounmanagedmods.h" +#include + +class Fallout4UnmangedMods : public GamebryoUnmangedMods +{ +public: + Fallout4UnmangedMods(const GameGamebryo* game); + ~Fallout4UnmangedMods(); + + virtual QStringList mods(bool onlyOfficial) const override; + virtual QStringList secondaryFiles(const QString& modName) const override; + virtual QString displayName(const QString& modName) const override; +}; + +#endif // FALLOUT4UNMANAGEDMODS_H diff --git a/libs/game_bethesda/src/games/fallout4/game_fallout4_en.ts b/libs/game_bethesda/src/games/fallout4/game_fallout4_en.ts new file mode 100644 index 0000000..1c57784 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/game_fallout4_en.ts @@ -0,0 +1,28 @@ + + + + + GameFallout4 + + + Fallout 4 Support Plugin + + + + + Adds support for the game Fallout 4. +Splash by %1 + + + + + sTestFile entries are present + + + + + <p>You have sTestFile settings in your Fallout4Custom.ini. These must be removed or the game will not read the plugins.txt file. Management is disabled.</p> + + + + diff --git a/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp b/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp new file mode 100644 index 0000000..8939a3c --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp @@ -0,0 +1,334 @@ +#include "gamefallout4.h" + +#include "fallout4bsainvalidation.h" +#include "fallout4dataarchives.h" +#include "fallout4moddatachecker.h" +#include "fallout4moddatacontent.h" +#include "fallout4savegame.h" +#include "fallout4scriptextender.h" +#include "fallout4unmanagedmods.h" + +#include "versioninfo.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "scopeguard.h" + +using namespace MOBase; + +const unsigned int GameFallout4::PROBLEM_TEST_FILE; + +GameFallout4::GameFallout4() {} + +bool GameFallout4::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(this, "fallout4custom.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(dataArchives.get(), this)); + + return true; +} + +QString GameFallout4::gameName() const +{ + return "Fallout 4"; +} + +void GameFallout4::detectGame() +{ + m_GamePath = identifyGamePath(); + m_MyGamesPath = determineMyGamesPath("Fallout4"); +} + +QList GameFallout4::executables() const +{ + return QList() + << ExecutableInfo("F4SE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Fallout 4", findInGameFolder(binaryName())) + << ExecutableInfo("Fallout Launcher", findInGameFolder(getLauncherName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("1946160") + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Fallout4\""); +} + +QList GameFallout4::executableForcedLoads() const +{ + return QList(); +} + +QString GameFallout4::name() const +{ + return "Fallout 4 Support Plugin"; +} + +QString GameFallout4::localizedName() const +{ + return tr("Fallout 4 Support Plugin"); +} + +QString GameFallout4::author() const +{ + return "Tannin & MO2 Team"; +} + +QString GameFallout4::description() const +{ + return tr("Adds support for the game Fallout 4.\n" + "Splash by %1") + .arg("nekoyoubi"); +} + +MOBase::VersionInfo GameFallout4::version() const +{ + return VersionInfo(1, 8, 0, VersionInfo::RELEASE_FINAL); +} + +QList GameFallout4::settings() const +{ + return QList(); +} + +MappingType GameFallout4::mappings() const +{ + MappingType result; + if (testFilePlugins().isEmpty()) { + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameShortName() + "/" + profileFile, + false}); + } + } + return result; +} + +void GameFallout4::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Fallout4", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/fallout4.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "fallout4_default.ini", + "fallout4.ini"); + } else { + copyToProfile(myGamesPath(), path, "fallout4.ini"); + } + + copyToProfile(myGamesPath(), path, "fallout4prefs.ini"); + copyToProfile(myGamesPath(), path, "fallout4custom.ini"); + } +} + +QString GameFallout4::savegameExtension() const +{ + return "fos"; +} + +QString GameFallout4::savegameSEExtension() const +{ + return "f4se"; +} + +std::shared_ptr +GameFallout4::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameFallout4::steamAPPId() const +{ + return "377160"; +} + +QStringList GameFallout4::testFilePlugins() const +{ + QStringList plugins; + if (m_Organizer != nullptr && m_Organizer->profile() != nullptr) { + QString customIni( + m_Organizer->profile()->absoluteIniFilePath("Fallout4Custom.ini")); + if (QFile(customIni).exists()) { + for (int i = 1; i <= 10; ++i) { + QString setting("sTestFile"); + setting += std::to_string(i); + QString plugin = GameGamebryo::readIniValue(customIni, "General", setting, ""); + if (!plugin.isEmpty() && !plugins.contains(plugin, Qt::CaseInsensitive)) { + plugins.append(plugin); + } + } + } + } + return plugins; +} + +QStringList GameFallout4::primaryPlugins() const +{ + QStringList plugins = {"fallout4.esm", "dlcrobot.esm", + "dlcworkshop01.esm", "dlccoast.esm", + "dlcworkshop02.esm", "dlcworkshop03.esm", + "dlcnukaworld.esm", "dlcultrahighresolution.esm"}; + + auto testPlugins = testFilePlugins(); + if (loadOrderMechanism() == LoadOrderMechanism::None) { + plugins << testPlugins; + } else { + plugins << CCPlugins(); + } + + return plugins; +} + +QStringList GameFallout4::gameVariants() const +{ + return {"Regular"}; +} + +QString GameFallout4::gameShortName() const +{ + return "Fallout4"; +} + +QString GameFallout4::gameNexusName() const +{ + return "fallout4"; +} + +QStringList GameFallout4::iniFiles() const +{ + return {"fallout4.ini", "fallout4prefs.ini", "fallout4custom.ini"}; +} + +QStringList GameFallout4::DLCPlugins() const +{ + return {"dlcrobot.esm", + "dlcworkshop01.esm", + "dlccoast.esm", + "dlcworkshop02.esm", + "dlcworkshop03.esm", + "dlcnukaworld.esm", + "dlcultrahighresolution.esm"}; +} + +QStringList GameFallout4::CCPlugins() const +{ + QStringList plugins = {}; + QFile file(gameDirectory().absoluteFilePath("Fallout4.ccc")); + if (file.open(QIODevice::ReadOnly)) { + ON_BLOCK_EXIT([&file]() { + file.close(); + }); + + if (file.size() == 0) { + return plugins; + } + while (!file.atEnd()) { + QByteArray line = file.readLine().trimmed(); + QString modName; + if ((line.size() > 0) && (line.at(0) != '#')) { + modName = QString::fromUtf8(line.constData()).toLower(); + } + + if (modName.size() > 0) { + if (!plugins.contains(modName, Qt::CaseInsensitive)) { + plugins.append(modName); + } + } + } + } + return plugins; +} + +IPluginGame::SortMechanism GameFallout4::sortMechanism() const +{ + if (!testFilePresent()) + return IPluginGame::SortMechanism::LOOT; + return IPluginGame::SortMechanism::NONE; +} + +IPluginGame::LoadOrderMechanism GameFallout4::loadOrderMechanism() const +{ + if (!testFilePresent()) + return IPluginGame::LoadOrderMechanism::PluginsTxt; + return IPluginGame::LoadOrderMechanism::None; +} + +int GameFallout4::nexusModOrganizerID() const +{ + return 28715; +} + +int GameFallout4::nexusGameID() const +{ + return 1151; +} + +// Start Diagnose +std::vector GameFallout4::activeProblems() const +{ + std::vector result; + if (m_Organizer->managedGame() == this) { + if (testFilePresent()) + result.push_back(PROBLEM_TEST_FILE); + } + return result; +} + +bool GameFallout4::testFilePresent() const +{ + if (!testFilePlugins().isEmpty()) + return true; + return false; +} + +QString GameFallout4::shortDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_TEST_FILE: + return tr("sTestFile entries are present"); + } + return QString(); +} + +QString GameFallout4::fullDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_TEST_FILE: { + return tr("

You have sTestFile settings in your " + "Fallout4Custom.ini. These must be removed or " + "the game will not read the plugins.txt file. " + "Management is disabled.

"); + } + } + return QString(); +} diff --git a/libs/game_bethesda/src/games/fallout4/gamefallout4.h b/libs/game_bethesda/src/games/fallout4/gamefallout4.h new file mode 100644 index 0000000..7fc6271 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/gamefallout4.h @@ -0,0 +1,73 @@ +#ifndef GAMEFALLOUT4_H +#define GAMEFALLOUT4_H + +#include "gamegamebryo.h" +#include "iplugindiagnose.h" + +#include +#include + +class GameFallout4 : public GameGamebryo, public MOBase::IPluginDiagnose +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginGame MOBase::IPluginDiagnose) + Q_PLUGIN_METADATA(IID "org.tannin.GameFallout4" FILE "gamefallout4.json") + +public: + GameFallout4(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: + QStringList testFilePlugins() const; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual void detectGame() override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QStringList gameVariants() const override; + virtual QString gameShortName() const override; + virtual QString gameNexusName() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual QStringList CCPlugins() const override; + virtual SortMechanism sortMechanism() const override; + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + +public: // IPluginDiagnose interface + virtual std::vector activeProblems() const override; + virtual QString shortDescription(unsigned int key) const override; + virtual QString fullDescription(unsigned int key) const override; + virtual bool hasGuidedFix(unsigned int key) const override { return false; } + virtual void startGuidedFix(unsigned int key) const override {} + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + virtual MappingType mappings() const override; + +protected: + std::shared_ptr makeSaveGame(QString filePath) const override; + QString savegameExtension() const override; + QString savegameSEExtension() const override; + +private: + bool testFilePresent() const; + +private: + static const unsigned int PROBLEM_TEST_FILE = 1; +}; + +#endif // GAMEFallout4_H diff --git a/libs/game_bethesda/src/games/fallout4/gamefallout4.json b/libs/game_bethesda/src/games/fallout4/gamefallout4.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/gamefallout4.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/fallout4/splash.png b/libs/game_bethesda/src/games/fallout4/splash.png new file mode 100644 index 0000000..d612e7b Binary files /dev/null and b/libs/game_bethesda/src/games/fallout4/splash.png differ diff --git a/libs/game_bethesda/src/games/fallout4london/CMakeLists.txt b/libs/game_bethesda/src/games/fallout4london/CMakeLists.txt new file mode 100644 index 0000000..7abda73 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_fo4london SHARED + fo4londonbsainvalidation.cpp + fo4londonbsainvalidation.h + fo4londondataarchives.cpp + fo4londondataarchives.h + fo4londonmoddatachecker.h + fo4londonmoddatacontent.h + fo4londonsavegame.cpp + fo4londonsavegame.h + fo4londonscriptextender.cpp + fo4londonscriptextender.h + fo4londonunmanagedmods.cpp + fo4londonunmanagedmods.h + gamefo4london.cpp + gamefo4london.h) +mo2_configure_plugin(game_fo4london NO_SOURCES WARNINGS 4) +target_link_libraries(game_fo4london PRIVATE game_creation) +mo2_install_plugin(game_fo4london) diff --git a/libs/game_bethesda/src/games/fallout4london/fo4london.qrc b/libs/game_bethesda/src/games/fallout4london/fo4london.qrc new file mode 100644 index 0000000..8f7bb2b --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4london.qrc @@ -0,0 +1,5 @@ + + + splash.png + + diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londonbsainvalidation.cpp b/libs/game_bethesda/src/games/fallout4london/fo4londonbsainvalidation.cpp new file mode 100644 index 0000000..45df74f --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londonbsainvalidation.cpp @@ -0,0 +1,68 @@ +#include "fo4londonbsainvalidation.h" + +#include "dummybsa.h" +#include "iplugingame.h" +#include "iprofile.h" +#include "registry.h" +#include +#include + +#include "gamegamebryo.h" + +Fallout4LondonBSAInvalidation::Fallout4LondonBSAInvalidation( + MOBase::DataArchives* dataArchives, MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "Fallout4Custom.ini", game) +{ + m_IniFileName = "Fallout4Custom.ini"; + m_Game = game; +} + +bool Fallout4LondonBSAInvalidation::isInvalidationBSA(const QString& bsaName) +{ + return false; +} + +QString Fallout4LondonBSAInvalidation::invalidationBSAName() const +{ + return ""; +} + +unsigned long Fallout4LondonBSAInvalidation::bsaVersion() const +{ + return 0x68; +} + +bool Fallout4LondonBSAInvalidation::prepareProfile(MOBase::IProfile* profile) +{ + bool dirty = false; + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : m_Game->documentsDirectory().absolutePath(); + QString iniFilePath = basePath + "/" + m_IniFileName; + + if (profile->invalidationActive(nullptr)) { + // write bInvalidateOlderFiles = 1, if needed + QString bInvalidateOlderFiles = GameGamebryo::readIniValue( + iniFilePath, "Archive", "bInvalidateOlderFiles", "0"); + if (bInvalidateOlderFiles.toLong() != 1) { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "bInvalidateOlderFiles", "1", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + QString sResourceDataDirsFinal = GameGamebryo::readIniValue( + iniFilePath, "Archive", "sResourceDataDirsFinal", "STRINGS\\"); + if (sResourceDataDirsFinal != "") { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "sResourceDataDirsFinal", "", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } + + return dirty; +} diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londonbsainvalidation.h b/libs/game_bethesda/src/games/fallout4london/fo4londonbsainvalidation.h new file mode 100644 index 0000000..5f5294b --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londonbsainvalidation.h @@ -0,0 +1,33 @@ +#ifndef FO4LONDONBSAINVALIDATION_H +#define FO4LONDONBSAINVALIDATION_H + +#include "fo4londondataarchives.h" +#include "gamebryobsainvalidation.h" +#include +#include + +#include + +namespace MOBase +{ +class IPluginGame; +} + +class Fallout4LondonBSAInvalidation : public GamebryoBSAInvalidation +{ +public: + Fallout4LondonBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + virtual bool isInvalidationBSA(const QString& bsaName) override; + virtual bool prepareProfile(MOBase::IProfile* profile) override; + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; + +private: + QString m_IniFileName; + MOBase::IPluginGame const* m_Game; +}; + +#endif // FO4LONDONBSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.cpp b/libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.cpp new file mode 100644 index 0000000..e631f75 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.cpp @@ -0,0 +1,48 @@ +#include "fo4londondataarchives.h" + +#include "iprofile.h" +#include + +QStringList Fallout4LondonDataArchives::vanillaArchives() const +{ + return {"Fallout4 - Textures1.ba2", "Fallout4 - Textures2.ba2", + "Fallout4 - Textures3.ba2", "Fallout4 - Textures4.ba2", + "Fallout4 - Textures5.ba2", "Fallout4 - Textures6.ba2", + "Fallout4 - Textures7.ba2", "Fallout4 - Textures8.ba2", + "Fallout4 - Textures9.ba2", "Fallout4 - Meshes.ba2", + "Fallout4 - MeshesExtra.ba2", "Fallout4 - Voices.ba2", + "Fallout4 - Sounds.ba2", "Fallout4 - Interface.ba2", + "Fallout4 - Animations.ba2", "Fallout4 - Materials.ba2", + "Fallout4 - Shaders.ba2", "Fallout4 - Startup.ba2", + "Fallout4 - Misc.ba2"}; +} + +QStringList Fallout4LondonDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini") + : localGameDirectory().absoluteFilePath("fallout4.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + + return result; +} + +void Fallout4LondonDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini") + : localGameDirectory().absoluteFilePath("fallout4.ini"); + if (list.length() > 255) { + int splitIdx = list.lastIndexOf(",", 256); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.h b/libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.h new file mode 100644 index 0000000..7e12b83 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.h @@ -0,0 +1,27 @@ +#ifndef FO4LONDONDATAARCHIVES_H +#define FO4LONDONDATAARCHIVES_H + +#include "gamebryodataarchives.h" + +namespace MOBase +{ +class IProfile; +} + +#include +#include + +class Fallout4LondonDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // FO4LONDONDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londonmoddatachecker.h b/libs/game_bethesda/src/games/fallout4london/fo4londonmoddatachecker.h new file mode 100644 index 0000000..c276281 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londonmoddatachecker.h @@ -0,0 +1,29 @@ +#ifndef FO4LONDON_MODATACHECKER_H +#define FO4LONDON_MODATACHECKER_H + +#include + +class Fallout4LondonModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{ + "interface", "meshes", "music", "scripts", "sound", "strings", + "textures", "trees", "video", "materials", "f4se", "distantlod", + "asi", "Tools", "MCM", "distantland", "mits", "dllplugins", + "CalienteTools", "shadersfx", "aaf"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "esl", "ba2", + "modgroups", "ini", "csg", "cdx"}; + return result; + } +}; + +#endif // FO4LONDON_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londonmoddatacontent.h b/libs/game_bethesda/src/games/fallout4london/fo4londonmoddatacontent.h new file mode 100644 index 0000000..91838da --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londonmoddatacontent.h @@ -0,0 +1,44 @@ +#ifndef FO4LONDON_MODDATACONTENT_H +#define FO4LONDON_MODDATACONTENT_H + +#include +#include + +class Fallout4LondonModDataContent : public GamebryoModDataContent +{ +protected: + enum Fallout4LondonContent + { + CONTENT_MATERIAL = CONTENT_NEXT_VALUE + }; + +public: + Fallout4LondonModDataContent(const MOBase::IGameFeatures* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + m_Enabled[CONTENT_SKYPROC] = false; + } + + std::vector getAllContents() const override + { + auto contents = GamebryoModDataContent::getAllContents(); + contents.push_back( + Content(CONTENT_MATERIAL, "Materials", ":/MO/gui/content/material")); + return contents; + } + + std::vector + getContentsFor(std::shared_ptr fileTree) const override + { + auto contents = GamebryoModDataContent::getContentsFor(fileTree); + for (auto e : *fileTree) { + if (e->compare("materials") == 0) { + contents.push_back(CONTENT_MATERIAL); + break; // Early break if you have nothing else to check. + } + } + return contents; + } +}; + +#endif // FO4LONDON_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londonsavegame.cpp b/libs/game_bethesda/src/games/fallout4london/fo4londonsavegame.cpp new file mode 100644 index 0000000..ead2d1a --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londonsavegame.cpp @@ -0,0 +1,108 @@ +#include "fo4londonsavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gamefo4london.h" + +Fallout4LondonSaveGame::Fallout4LondonSaveGame(QString const& fileName, + GameFallout4London const* game) + : GamebryoSaveGame(fileName, game, true) +{ + FileWrapper file(getFilepath(), "FO4_SAVEGAME"); + + FILETIME creationTime; + fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, + creationTime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&creationTime, &ctime); + + setCreationTime(ctime); +} + +void Fallout4LondonSaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& saveNumber, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, FILETIME& creationTime) const +{ + file.skip(); // header size + file.skip(); // header version + file.read(saveNumber); + + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString ignore; + file.read(ignore); // playtime as ascii hh.mm.ss + file.read(ignore); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); +} + +std::unique_ptr +Fallout4LondonSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "FO4_SAVEGAME"); // 10bytes + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation, + dummyTime); + } + + QString ignore; + std::unique_ptr fields = std::make_unique(); + + fields->Screenshot = file.readImage(384, true); + + uint8_t saveGameVersion = file.readChar(); + file.read(ignore); // game version + file.skip(); // plugin info size + + fields->Plugins = file.readPlugins(); + if (saveGameVersion >= 68) { + fields->LightPlugins = file.readLightPlugins(); + } + + return fields; +} diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londonsavegame.h b/libs/game_bethesda/src/games/fallout4london/fo4londonsavegame.h new file mode 100644 index 0000000..1a892dc --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londonsavegame.h @@ -0,0 +1,23 @@ +#ifndef FO4LONDONSAVEGAME_H +#define FO4LONDONSAVEGAME_H + +#include "gamebryosavegame.h" +#include "windows_compat.h" + +class GameFallout4London; + +class Fallout4LondonSaveGame : public GamebryoSaveGame +{ +public: + Fallout4LondonSaveGame(QString const& fileName, GameFallout4London const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& file, uint32_t& saveNumber, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation, FILETIME& creationTime) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // FO4LONDONSAVEGAME_H diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londonscriptextender.cpp b/libs/game_bethesda/src/games/fallout4london/fo4londonscriptextender.cpp new file mode 100644 index 0000000..109ca51 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londonscriptextender.cpp @@ -0,0 +1,18 @@ +#include "fo4londonscriptextender.h" + +#include +#include + +Fallout4LondonScriptExtender::Fallout4LondonScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString Fallout4LondonScriptExtender::BinaryName() const +{ + return "f4se_loader.exe"; +} + +QString Fallout4LondonScriptExtender::PluginPath() const +{ + return "f4se/plugins"; +} diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londonscriptextender.h b/libs/game_bethesda/src/games/fallout4london/fo4londonscriptextender.h new file mode 100644 index 0000000..20a57e5 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londonscriptextender.h @@ -0,0 +1,17 @@ +#ifndef FO4LONDONSCRIPTEXTENDER_H +#define FO4LONDONSCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class Fallout4LondonScriptExtender : public GamebryoScriptExtender +{ +public: + Fallout4LondonScriptExtender(GameGamebryo const* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // FO4LONDONSCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londonunmanagedmods.cpp b/libs/game_bethesda/src/games/fallout4london/fo4londonunmanagedmods.cpp new file mode 100644 index 0000000..f9a6631 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londonunmanagedmods.cpp @@ -0,0 +1,63 @@ +#include "fo4londonunmanagedmods.h" + +Fallout4LondonUnmangedMods::Fallout4LondonUnmangedMods(const GameGamebryo* game) + : GamebryoUnmangedMods(game) +{} + +Fallout4LondonUnmangedMods::~Fallout4LondonUnmangedMods() {} + +QStringList Fallout4LondonUnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QDir dataDir(game()->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + + return result; +} + +QStringList Fallout4LondonUnmangedMods::secondaryFiles(const QString& modName) const +{ + // file extension in FO4 is .ba2 instead of bsa + QStringList archives; + QDir dataDir = game()->dataDirectory(); + for (const QString& archiveName : dataDir.entryList({modName + "*.ba2"})) { + archives.append(dataDir.absoluteFilePath(archiveName)); + } + return archives; +} + +QString Fallout4LondonUnmangedMods::displayName(const QString& modName) const +{ + // unlike in earlier games, in fallout 4 the file name doesn't correspond to + // the public name + if (modName.compare("dlcrobot", Qt::CaseInsensitive) == 0) { + return "Automatron"; + } else if (modName.compare("dlcworkshop01", Qt::CaseInsensitive) == 0) { + return "Wasteland Workshop"; + } else if (modName.compare("dlccoast", Qt::CaseInsensitive) == 0) { + return "Far Harbor"; + } else if (modName.compare("dlcworkshop02", Qt::CaseInsensitive) == 0) { + return "Contraptions Workshop"; + } else if (modName.compare("dlcworkshop03", Qt::CaseInsensitive) == 0) { + return "Vault-Tec Workshop"; + } else if (modName.compare("dlcnukaworld", Qt::CaseInsensitive) == 0) { + return "Nuka-World"; + } else if (modName.compare("dlcultrahighresolution", Qt::CaseInsensitive) == 0) { + return "Ultra High Resolution Texture Pack"; + } else { + return modName; + } +} diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londonunmanagedmods.h b/libs/game_bethesda/src/games/fallout4london/fo4londonunmanagedmods.h new file mode 100644 index 0000000..1d98fea --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londonunmanagedmods.h @@ -0,0 +1,18 @@ +#ifndef FO4LONDONUNMANAGEDMODS_H +#define FO4LONDONUNMANAGEDMODS_H + +#include "gamebryounmanagedmods.h" +#include + +class Fallout4LondonUnmangedMods : public GamebryoUnmangedMods +{ +public: + Fallout4LondonUnmangedMods(const GameGamebryo* game); + ~Fallout4LondonUnmangedMods(); + + virtual QStringList mods(bool onlyOfficial) const override; + virtual QStringList secondaryFiles(const QString& modName) const override; + virtual QString displayName(const QString& modName) const override; +}; + +#endif // FO4LONDONUNMANAGEDMODS_H diff --git a/libs/game_bethesda/src/games/fallout4london/game_fo4london_en.ts b/libs/game_bethesda/src/games/fallout4london/game_fo4london_en.ts new file mode 100644 index 0000000..73a7ac2 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/game_fo4london_en.ts @@ -0,0 +1,27 @@ + + + + + GameFallout4London + + + Fallout 4 London Support Plugin + + + + + Adds support for the game Fallout 4 London. + + + + + sTestFile entries are present + + + + + <p>You have sTestFile settings in your Fallout4Custom.ini. These must be removed or the game will not read the plugins.txt file. Management is disabled.</p> + + + + diff --git a/libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp b/libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp new file mode 100644 index 0000000..0fa774a --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp @@ -0,0 +1,366 @@ +#include "gamefo4london.h" + +#include "fo4londonbsainvalidation.h" +#include "fo4londondataarchives.h" +#include "fo4londonmoddatachecker.h" +#include "fo4londonmoddatacontent.h" +#include "fo4londonsavegame.h" +#include "fo4londonscriptextender.h" +#include "fo4londonunmanagedmods.h" + +#include "versioninfo.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "scopeguard.h" + +using namespace MOBase; + +const unsigned int GameFallout4London::PROBLEM_TEST_FILE; + +GameFallout4London::GameFallout4London() {} + +bool GameFallout4London::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature( + std::make_shared(this, "fo4londoncustom.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(dataArchives.get(), this)); + + return true; +} + +QString GameFallout4London::gameName() const +{ + return "Fallout 4 London"; +} + +void GameFallout4London::detectGame() +{ + m_GamePath = identifyGamePath(); + m_MyGamesPath = determineMyGamesPath("Fallout4"); +} + +QString GameFallout4London::identifyGamePath() const +{ +#ifdef _WIN32 + // TODO: Add GOG support + QString path = "Software\\Bethesda Softworks\\Fallout4"; + return findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(), + L"Installed Path"); +#else + return GameGamebryo::identifyGamePath(); +#endif +} + +QList GameFallout4London::executables() const +{ + return QList() + << ExecutableInfo("F4SE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Fallout 4 London", findInGameFolder(binaryName())) + << ExecutableInfo("Fallout Launcher", findInGameFolder(getLauncherName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("1946160") + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Fallout4\""); +} + +QList GameFallout4London::executableForcedLoads() const +{ + return QList(); +} + +QString GameFallout4London::name() const +{ + return "Fallout 4 London Support Plugin"; +} + +QString GameFallout4London::localizedName() const +{ + return tr("Fallout 4 London Support Plugin"); +} + +QString GameFallout4London::author() const +{ + return "MO2 Team"; +} + +QString GameFallout4London::description() const +{ + return tr("Adds support for the game Fallout 4 London."); +} + +MOBase::VersionInfo GameFallout4London::version() const +{ + return VersionInfo(0, 0, 1, VersionInfo::RELEASE_PREALPHA); +} + +QList GameFallout4London::settings() const +{ + return QList(); +} + +MappingType GameFallout4London::mappings() const +{ + MappingType result; + if (testFilePlugins().isEmpty()) { + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/Fallout4/" + profileFile, false}); + } + } + return result; +} + +void GameFallout4London::initializeProfile(const QDir& path, + ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Fallout4", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/fallout4.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "fallout4_default.ini", + "fallout4.ini"); + } else { + copyToProfile(myGamesPath(), path, "fallout4.ini"); + } + + copyToProfile(myGamesPath(), path, "fallout4prefs.ini"); + copyToProfile(myGamesPath(), path, "fallout4custom.ini"); + } +} + +QString GameFallout4London::savegameExtension() const +{ + return "fos"; +} + +QString GameFallout4London::savegameSEExtension() const +{ + return "f4se"; +} + +std::shared_ptr +GameFallout4London::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameFallout4London::steamAPPId() const +{ + return "377160"; +} + +QStringList GameFallout4London::testFilePlugins() const +{ + QStringList plugins; + if (m_Organizer != nullptr && m_Organizer->profile() != nullptr) { + QString customIni( + m_Organizer->profile()->absoluteIniFilePath("Fallout4Custom.ini")); + if (QFile(customIni).exists()) { + for (int i = 1; i <= 10; ++i) { + QString setting("sTestFile"); + setting += std::to_string(i); + QString plugin = GameGamebryo::readIniValue(customIni, "General", setting, ""); + if (!plugin.isEmpty() && !plugins.contains(plugin, Qt::CaseInsensitive)) { + plugins.append(plugin); + } + } + } + } + return plugins; +} + +QStringList GameFallout4London::primaryPlugins() const +{ + QStringList plugins = {"fallout4.esm", "dlcrobot.esm", + "dlcworkshop01.esm", "dlccoast.esm", + "dlcworkshop02.esm", "dlcworkshop03.esm", + "dlcnukaworld.esm", "dlcultrahighresolution.esm"}; + + auto testPlugins = testFilePlugins(); + if (loadOrderMechanism() == LoadOrderMechanism::None) { + plugins << testPlugins; + } else { + plugins << CCPlugins(); + } + + return plugins; +} + +QStringList GameFallout4London::enabledPlugins() const +{ + return {"bakaframework.esm", "londonworldspace.esm", "londonworldspace-dlcblock.esp"}; +} + +QStringList GameFallout4London::gameVariants() const +{ + return {"Regular"}; +} + +QString GameFallout4London::gameShortName() const +{ + return "Fallout4London"; +} + +QStringList GameFallout4London::validShortNames() const +{ + return {"Fallout4"}; +} + +QString GameFallout4London::gameNexusName() const +{ + return "fallout4london"; +} + +QString GameFallout4London::binaryName() const +{ + return "Fallout4.exe"; +} + +QString GameFallout4London::getLauncherName() const +{ + return "Fallout4Launcher.exe"; +} + +QStringList GameFallout4London::iniFiles() const +{ + return {"fallout4.ini", "fallout4prefs.ini", "fallout4custom.ini"}; +} + +QStringList GameFallout4London::DLCPlugins() const +{ + return {"dlcrobot.esm", + "dlcworkshop01.esm", + "dlccoast.esm", + "dlcworkshop02.esm", + "dlcworkshop03.esm", + "dlcnukaworld.esm", + "dlcultrahighresolution.esm"}; +} + +QStringList GameFallout4London::CCPlugins() const +{ + QStringList plugins = {}; + QFile file(gameDirectory().absoluteFilePath("Fallout4.ccc")); + if (file.open(QIODevice::ReadOnly)) { + ON_BLOCK_EXIT([&file]() { + file.close(); + }); + + if (file.size() == 0) { + return plugins; + } + while (!file.atEnd()) { + QByteArray line = file.readLine().trimmed(); + QString modName; + if ((line.size() > 0) && (line.at(0) != '#')) { + modName = QString::fromUtf8(line.constData()).toLower(); + } + + if (modName.size() > 0) { + if (!plugins.contains(modName, Qt::CaseInsensitive)) { + plugins.append(modName); + } + } + } + } + return plugins; +} + +IPluginGame::SortMechanism GameFallout4London::sortMechanism() const +{ + if (!testFilePresent()) + return IPluginGame::SortMechanism::LOOT; + return IPluginGame::SortMechanism::NONE; +} + +IPluginGame::LoadOrderMechanism GameFallout4London::loadOrderMechanism() const +{ + if (!testFilePresent()) + return IPluginGame::LoadOrderMechanism::PluginsTxt; + return IPluginGame::LoadOrderMechanism::None; +} + +int GameFallout4London::nexusModOrganizerID() const +{ + return 28715; +} + +int GameFallout4London::nexusGameID() const +{ + return 1151; +} + +// Start Diagnose +std::vector GameFallout4London::activeProblems() const +{ + std::vector result; + if (m_Organizer->managedGame() == this) { + if (testFilePresent()) + result.push_back(PROBLEM_TEST_FILE); + } + return result; +} + +bool GameFallout4London::testFilePresent() const +{ + if (!testFilePlugins().isEmpty()) + return true; + return false; +} + +QString GameFallout4London::shortDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_TEST_FILE: + return tr("sTestFile entries are present"); + } + return QString(); +} + +QString GameFallout4London::fullDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_TEST_FILE: { + return tr("

You have sTestFile settings in your " + "Fallout4Custom.ini. These must be removed or " + "the game will not read the plugins.txt file. " + "Management is disabled.

"); + } + } + return QString(); +} diff --git a/libs/game_bethesda/src/games/fallout4london/gamefo4london.h b/libs/game_bethesda/src/games/fallout4london/gamefo4london.h new file mode 100644 index 0000000..fc0c85f --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/gamefo4london.h @@ -0,0 +1,78 @@ +#ifndef GAMEFO4LONDON_H +#define GAMEFO4LONDON_H + +#include "gamegamebryo.h" +#include "iplugindiagnose.h" + +#include +#include + +class GameFallout4London : public GameGamebryo, public MOBase::IPluginDiagnose +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginGame MOBase::IPluginDiagnose) + Q_PLUGIN_METADATA(IID "org.tannin.GameFallout4London" FILE "gamefo4london.json") + +public: + GameFallout4London(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: + QStringList testFilePlugins() const; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual QString identifyGamePath() const override; + virtual void detectGame() override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QStringList enabledPlugins() const override; + virtual QStringList gameVariants() const override; + virtual QString gameShortName() const override; + virtual QStringList validShortNames() const override; + virtual QString gameNexusName() const override; + virtual QString binaryName() const override; + virtual QString getLauncherName() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual QStringList CCPlugins() const override; + virtual SortMechanism sortMechanism() const override; + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + +public: // IPluginDiagnose interface + virtual std::vector activeProblems() const override; + virtual QString shortDescription(unsigned int key) const override; + virtual QString fullDescription(unsigned int key) const override; + virtual bool hasGuidedFix(unsigned int key) const override { return false; } + virtual void startGuidedFix(unsigned int key) const override {} + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + virtual MappingType mappings() const override; + +protected: + std::shared_ptr makeSaveGame(QString filePath) const override; + QString savegameExtension() const override; + QString savegameSEExtension() const override; + +private: + bool testFilePresent() const; + +private: + static const unsigned int PROBLEM_TEST_FILE = 1; +}; + +#endif // GAMEFallout4London_H diff --git a/libs/game_bethesda/src/games/fallout4london/gamefo4london.json b/libs/game_bethesda/src/games/fallout4london/gamefo4london.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/gamefo4london.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/fallout4london/splash.png b/libs/game_bethesda/src/games/fallout4london/splash.png new file mode 100644 index 0000000..d612e7b Binary files /dev/null and b/libs/game_bethesda/src/games/fallout4london/splash.png differ diff --git a/libs/game_bethesda/src/games/fallout4vr/CMakeLists.txt b/libs/game_bethesda/src/games/fallout4vr/CMakeLists.txt new file mode 100644 index 0000000..4558388 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_fallout4vr SHARED + fallout4vrdataarchives.cpp + fallout4vrdataarchives.h + fallout4vrgameplugins.cpp + fallout4vrgameplugins.h + fallout4vrmoddatachecker.h + fallout4vrmoddatacontent.h + fallout4vrsavegame.cpp + fallout4vrsavegame.h + fallout4vrscriptextender.cpp + fallout4vrscriptextender.h + fallout4vrunmanagedmods.cpp + fallout4vrunmanagedmods.h + gamefallout4vr.cpp + gamefallout4vr.h +) +mo2_configure_plugin(game_fallout4vr NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_fallout4vr PRIVATE game_creation) +mo2_install_plugin(game_fallout4vr) diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vr.qrc b/libs/game_bethesda/src/games/fallout4vr/fallout4vr.qrc new file mode 100644 index 0000000..75ba055 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vr.qrc @@ -0,0 +1,5 @@ + + + splash.png + + diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.cpp b/libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.cpp new file mode 100644 index 0000000..58a9a09 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.cpp @@ -0,0 +1,49 @@ +#include "fallout4vrdataarchives.h" + +#include "iprofile.h" +#include + +QStringList Fallout4VRDataArchives::vanillaArchives() const +{ + return {"Fallout4 - Textures1.ba2", "Fallout4 - Textures2.ba2", + "Fallout4 - Textures3.ba2", "Fallout4 - Textures4.ba2", + "Fallout4 - Textures5.ba2", "Fallout4 - Textures6.ba2", + "Fallout4 - Textures7.ba2", "Fallout4 - Textures8.ba2", + "Fallout4 - Textures9.ba2", "Fallout4 - Meshes.ba2", + "Fallout4 - MeshesExtra.ba2", "Fallout4 - Voices.ba2", + "Fallout4 - Sounds.ba2", "Fallout4 - Interface.ba2", + "Fallout4 - Animations.ba2", "Fallout4 - Materials.ba2", + "Fallout4 - Shaders.ba2", "Fallout4 - Startup.ba2", + "Fallout4 - Misc.ba2", "Fallout4_VR - Main.ba2", + "Fallout4_VR - Shaders.ba2", "Fallout4_VR - Textures.ba2"}; +} + +QStringList Fallout4VRDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini") + : localGameDirectory().absoluteFilePath("fallout4.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + + return result; +} + +void Fallout4VRDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini") + : localGameDirectory().absoluteFilePath("fallout4.ini"); + if (list.length() > 255) { + int splitIdx = list.lastIndexOf(",", 256); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.h b/libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.h new file mode 100644 index 0000000..71d0b3d --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.h @@ -0,0 +1,27 @@ +#ifndef FALLOUT4VRDATAARCHIVES_H +#define FALLOUT4VRDATAARCHIVES_H + +#include "gamebryodataarchives.h" + +namespace MOBase +{ +class IProfile; +} + +#include +#include + +class Fallout4VRDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // Fallout4VRDataArchives_H diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrgameplugins.cpp b/libs/game_bethesda/src/games/fallout4vr/fallout4vrgameplugins.cpp new file mode 100644 index 0000000..e40a3ae --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrgameplugins.cpp @@ -0,0 +1,15 @@ +#include "fallout4vrgameplugins.h" + +using namespace MOBase; + +Fallout4VRGamePlugins::Fallout4VRGamePlugins(MOBase::IOrganizer* organizer) + : CreationGamePlugins(organizer) +{} + +bool Fallout4VRGamePlugins::lightPluginsAreSupported() +{ + auto files = m_Organizer->findFiles("f4se\\plugins", {"falloutvresl.dll"}); + if (files.isEmpty()) + return false; + return true; +} diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrgameplugins.h b/libs/game_bethesda/src/games/fallout4vr/fallout4vrgameplugins.h new file mode 100644 index 0000000..741378f --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrgameplugins.h @@ -0,0 +1,19 @@ +#ifndef _FALLOUT4VRGAMEPLUGINS_H +#define _FALLOUT4VRGAMEPLUGINS_H + +#include + +#include +#include + +class Fallout4VRGamePlugins : public CreationGamePlugins +{ + +public: + Fallout4VRGamePlugins(MOBase::IOrganizer* organizer); + +protected: + virtual bool lightPluginsAreSupported() override; +}; + +#endif // _FALLOUT4VRGAMEPLUGINS_H diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrmoddatachecker.h b/libs/game_bethesda/src/games/fallout4vr/fallout4vrmoddatachecker.h new file mode 100644 index 0000000..4517910 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrmoddatachecker.h @@ -0,0 +1,30 @@ +#ifndef FALLOUT4VR_MODATACHECKER_H +#define FALLOUT4VR_MODATACHECKER_H + +#include + +class Fallout4VRModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{ + "interface", "meshes", "music", "scripts", + "sound", "strings", "textures", "trees", + "video", "materials", "f4se", "distantlod", + "asi", "Tools", "MCM", "distantland", + "mits", "dllplugins", "CalienteTools", "NetScriptFramework", + "shadersfx", "aaf"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "ba2", "modgroups", "ini"}; + return result; + } +}; + +#endif // FALLOUT4VR_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrmoddatacontent.h b/libs/game_bethesda/src/games/fallout4vr/fallout4vrmoddatacontent.h new file mode 100644 index 0000000..4244a95 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrmoddatacontent.h @@ -0,0 +1,44 @@ +#ifndef FALLOUT4VR_MODDATACONTENT_H +#define FALLOUT4VR_MODDATACONTENT_H + +#include +#include + +class Fallout4VRModDataContent : public GamebryoModDataContent +{ +protected: + enum Fallout4Content + { + CONTENT_MATERIAL = CONTENT_NEXT_VALUE + }; + +public: + Fallout4VRModDataContent(MOBase::IGameFeatures const* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + m_Enabled[CONTENT_SKYPROC] = false; + } + + std::vector getAllContents() const override + { + auto contents = GamebryoModDataContent::getAllContents(); + contents.push_back( + Content(CONTENT_MATERIAL, "Materials", ":/MO/gui/content/material")); + return contents; + } + + std::vector + getContentsFor(std::shared_ptr fileTree) const override + { + auto contents = GamebryoModDataContent::getContentsFor(fileTree); + for (auto e : *fileTree) { + if (e->compare("materials") == 0) { + contents.push_back(CONTENT_MATERIAL); + break; // Early break if you have nothing else to check. + } + } + return contents; + } +}; + +#endif // FALLOUT4VR_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrsavegame.cpp b/libs/game_bethesda/src/games/fallout4vr/fallout4vrsavegame.cpp new file mode 100644 index 0000000..cf29aae --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrsavegame.cpp @@ -0,0 +1,108 @@ +#include "fallout4vrsavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gamefallout4vr.h" + +Fallout4VRSaveGame::Fallout4VRSaveGame(QString const& fileName, + GameFallout4VR const* game) + : GamebryoSaveGame(fileName, game, true) +{ + FileWrapper file(getFilepath(), "FO4_SAVEGAME"); + + FILETIME creationTime; + fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, + creationTime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&creationTime, &ctime); + + setCreationTime(ctime); +} + +void Fallout4VRSaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& saveNumber, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, FILETIME& creationTime) const +{ + file.skip(); // header size + file.skip(); // header version + file.read(saveNumber); + + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString ignore; + file.read(ignore); // playtime as ascii hh.mm.ss + file.read(ignore); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); +} + +std::unique_ptr +Fallout4VRSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "FO4_SAVEGAME"); // 10bytes + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation, + dummyTime); + } + + QString ignore; + std::unique_ptr fields = std::make_unique(); + + fields->Screenshot = file.readImage(384, true); + + uint8_t saveGameVersion = file.readChar(); + file.read(ignore); // game version + file.skip(); // plugin info size + + fields->Plugins = file.readPlugins(); + if (saveGameVersion >= 68) { + fields->LightPlugins = file.readLightPlugins(); + } + + return fields; +} diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrsavegame.h b/libs/game_bethesda/src/games/fallout4vr/fallout4vrsavegame.h new file mode 100644 index 0000000..e39456c --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrsavegame.h @@ -0,0 +1,23 @@ +#ifndef FALLOUT4VRSAVEGAME_H +#define FALLOUT4VRSAVEGAME_H + +#include "gamebryosavegame.h" +#include "windows_compat.h" + +class GameFallout4VR; + +class Fallout4VRSaveGame : public GamebryoSaveGame +{ +public: + Fallout4VRSaveGame(QString const& fileName, GameFallout4VR const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& file, uint32_t& saveNumber, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation, FILETIME& creationTime) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // FALLOUT4VRSAVEGAME_H diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrscriptextender.cpp b/libs/game_bethesda/src/games/fallout4vr/fallout4vrscriptextender.cpp new file mode 100644 index 0000000..6508c6a --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrscriptextender.cpp @@ -0,0 +1,18 @@ +#include "fallout4vrscriptextender.h" + +#include +#include + +Fallout4VRScriptExtender::Fallout4VRScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString Fallout4VRScriptExtender::BinaryName() const +{ + return "f4se_loader.exe"; +} + +QString Fallout4VRScriptExtender::PluginPath() const +{ + return "f4se/plugins"; +} diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrscriptextender.h b/libs/game_bethesda/src/games/fallout4vr/fallout4vrscriptextender.h new file mode 100644 index 0000000..f7f8587 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrscriptextender.h @@ -0,0 +1,17 @@ +#ifndef FALLOUT4VRSCRIPTEXTENDER_H +#define FALLOUT4VRSCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class Fallout4VRScriptExtender : public GamebryoScriptExtender +{ +public: + Fallout4VRScriptExtender(GameGamebryo const* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // FALLOUT4SCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrunmanagedmods.cpp b/libs/game_bethesda/src/games/fallout4vr/fallout4vrunmanagedmods.cpp new file mode 100644 index 0000000..a217ef0 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrunmanagedmods.cpp @@ -0,0 +1,63 @@ +#include "fallout4vrunmanagedmods.h" + +Fallout4VRUnmangedMods::Fallout4VRUnmangedMods(const GameGamebryo* game) + : GamebryoUnmangedMods(game) +{} + +Fallout4VRUnmangedMods::~Fallout4VRUnmangedMods() {} + +QStringList Fallout4VRUnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QDir dataDir(game()->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + + return result; +} + +QStringList Fallout4VRUnmangedMods::secondaryFiles(const QString& modName) const +{ + // file extension in FO4 is .ba2 instead of bsa + QStringList archives; + QDir dataDir = game()->dataDirectory(); + for (const QString& archiveName : dataDir.entryList({modName + "*.ba2"})) { + archives.append(dataDir.absoluteFilePath(archiveName)); + } + return archives; +} + +QString Fallout4VRUnmangedMods::displayName(const QString& modName) const +{ + // unlike in earlier games, in fallout 4 the file name doesn't correspond to + // the public name + if (modName.compare("dlcrobot", Qt::CaseInsensitive) == 0) { + return "Automatron"; + } else if (modName.compare("dlcworkshop01", Qt::CaseInsensitive) == 0) { + return "Wasteland Workshop"; + } else if (modName.compare("dlccoast", Qt::CaseInsensitive) == 0) { + return "Far Harbor"; + } else if (modName.compare("dlcworkshop02", Qt::CaseInsensitive) == 0) { + return "Contraptions Workshop"; + } else if (modName.compare("dlcworkshop03", Qt::CaseInsensitive) == 0) { + return "Vault-Tec Workshop"; + } else if (modName.compare("dlcnukaworld", Qt::CaseInsensitive) == 0) { + return "Nuka-World"; + } else if (modName.compare("dlcultrahighresolution", Qt::CaseInsensitive) == 0) { + return "Ultra High Resolution Texture Pack"; + } else { + return modName; + } +} diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrunmanagedmods.h b/libs/game_bethesda/src/games/fallout4vr/fallout4vrunmanagedmods.h new file mode 100644 index 0000000..f0f15fd --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrunmanagedmods.h @@ -0,0 +1,18 @@ +#ifndef FALLOUT4VRUNMANAGEDMODS_H +#define FALLOUT4VRUNMANAGEDMODS_H + +#include "gamebryounmanagedmods.h" +#include + +class Fallout4VRUnmangedMods : public GamebryoUnmangedMods +{ +public: + Fallout4VRUnmangedMods(const GameGamebryo* game); + ~Fallout4VRUnmangedMods(); + + virtual QStringList mods(bool onlyOfficial) const override; + virtual QStringList secondaryFiles(const QString& modName) const override; + virtual QString displayName(const QString& modName) const override; +}; + +#endif // FALLOUT4UNMANAGEDMODS_H diff --git a/libs/game_bethesda/src/games/fallout4vr/game_fallout4vr_en.ts b/libs/game_bethesda/src/games/fallout4vr/game_fallout4vr_en.ts new file mode 100644 index 0000000..4044f20 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/game_fallout4vr_en.ts @@ -0,0 +1,18 @@ + + + + + GameFallout4VR + + + Fallout 4 VR Support Plugin + + + + + Adds support for the game Fallout 4 VR. +Splash by %1 + + + + diff --git a/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp b/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp new file mode 100644 index 0000000..bd2448b --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp @@ -0,0 +1,251 @@ +#include "gamefallout4vr.h" + +#include "fallout4vrdataarchives.h" +#include "fallout4vrgameplugins.h" +#include "fallout4vrmoddatachecker.h" +#include "fallout4vrmoddatacontent.h" +#include "fallout4vrsavegame.h" +#include "fallout4vrunmanagedmods.h" + +#include "versioninfo.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "scopeguard.h" + +using namespace MOBase; + +GameFallout4VR::GameFallout4VR() {} + +bool GameFallout4VR::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "fallout4custom.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameFallout4VR::gameName() const +{ + return "Fallout 4 VR"; +} + +void GameFallout4VR::detectGame() +{ + m_GamePath = identifyGamePath(); + m_MyGamesPath = determineMyGamesPath("Fallout4VR"); +} + +QList GameFallout4VR::executables() const +{ + return QList() + << ExecutableInfo("Fallout 4 VR", findInGameFolder(binaryName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Fallout4VR\""); +} + +QList GameFallout4VR::executableForcedLoads() const +{ + return QList(); +} + +QString GameFallout4VR::name() const +{ + return "Fallout 4 VR Support Plugin"; +} + +QString GameFallout4VR::localizedName() const +{ + return tr("Fallout 4 VR Support Plugin"); +} + +QString GameFallout4VR::author() const +{ + return "MO2 Contibutors"; +} + +QString GameFallout4VR::description() const +{ + return tr("Adds support for the game Fallout 4 VR.\n" + "Splash by %1") + .arg("nekoyoubi"); +} + +MOBase::VersionInfo GameFallout4VR::version() const +{ + return VersionInfo(1, 6, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameFallout4VR::settings() const +{ + return QList(); +} + +void GameFallout4VR::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Fallout4VR", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/fallout4.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "fallout4.ini"); + } else { + copyToProfile(myGamesPath(), path, "fallout4.ini"); + } + + copyToProfile(myGamesPath(), path, "fallout4prefs.ini"); + copyToProfile(myGamesPath(), path, "fallout4custom.ini"); + } +} + +QString GameFallout4VR::savegameExtension() const +{ + return "fos"; +} + +QString GameFallout4VR::savegameSEExtension() const +{ + return "f4se"; +} + +std::shared_ptr +GameFallout4VR::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameFallout4VR::steamAPPId() const +{ + return "611660"; +} + +QStringList GameFallout4VR::primaryPlugins() const +{ + QStringList plugins = {"fallout4.esm", "fallout4_vr.esm"}; + + plugins.append(CCPlugins()); + + return plugins; +} + +QStringList GameFallout4VR::gameVariants() const +{ + return {"Regular"}; +} + +QString GameFallout4VR::gameShortName() const +{ + return "Fallout4VR"; +} + +QStringList GameFallout4VR::validShortNames() const +{ + return {"Fallout4"}; +} + +QString GameFallout4VR::gameNexusName() const +{ + return QString(); +} + +QStringList GameFallout4VR::iniFiles() const +{ + return {"fallout4.ini", "fallout4custom.ini", "fallout4prefs.ini"}; +} + +QStringList GameFallout4VR::DLCPlugins() const +{ + return {"dlcrobot.esm", + "dlcworkshop01.esm", + "dlccoast.esm", + "dlcworkshop02.esm", + "dlcworkshop03.esm", + "dlcnukaworld.esm", + "dlcultrahighresolution.esm"}; +} + +QStringList GameFallout4VR::CCPlugins() const +{ + QStringList plugins = {}; + QFile file(gameDirectory().absoluteFilePath("Fallout4.ccc")); + if (file.open(QIODevice::ReadOnly)) { + ON_BLOCK_EXIT([&file]() { + file.close(); + }); + + if (file.size() == 0) { + return plugins; + } + while (!file.atEnd()) { + QByteArray line = file.readLine().trimmed(); + QString modName; + if ((line.size() > 0) && (line.at(0) != '#')) { + modName = QString::fromUtf8(line.constData()).toLower(); + } + + if (modName.size() > 0) { + if (!plugins.contains(modName, Qt::CaseInsensitive)) { + plugins.append(modName); + } + } + } + } + return plugins; +} + +IPluginGame::LoadOrderMechanism GameFallout4VR::loadOrderMechanism() const +{ + return IPluginGame::LoadOrderMechanism::PluginsTxt; +} + +int GameFallout4VR::nexusModOrganizerID() const +{ + return 0; //... +} + +int GameFallout4VR::nexusGameID() const +{ + return 0; +} + +QString GameFallout4VR::getLauncherName() const +{ + return binaryName(); // Fallout 4 VR has no Launcher, so we just return the name of + // the game binary +} + +QString GameFallout4VR::identifyGamePath() const +{ +#ifdef _WIN32 + QString path = "Software\\Bethesda Softworks\\" + gameName(); + return findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(), + L"Installed Path"); +#else + return GameGamebryo::identifyGamePath(); +#endif +} diff --git a/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.h b/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.h new file mode 100644 index 0000000..acd90d1 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.h @@ -0,0 +1,59 @@ +#ifndef GAMEFALLOUT4VR_H +#define GAMEFALLOUT4VR_H + +#include "gamegamebryo.h" + +#include +#include + +class GameFallout4VR : public GameGamebryo +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "org.tannin.GameFallout4VR" FILE "gamefallout4vr.json") + +public: + GameFallout4VR(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual void detectGame() override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QStringList gameVariants() const override; + virtual QString gameShortName() const override; + virtual QStringList primarySources() const override { return validShortNames(); }; + virtual QStringList validShortNames() const override; + virtual QString gameNexusName() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual QStringList CCPlugins() const override; + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + virtual QString getLauncherName() const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + +protected: + std::shared_ptr makeSaveGame(QString filePath) const override; + QString savegameExtension() const override; + QString savegameSEExtension() const override; + + virtual QString identifyGamePath() const override; +}; + +#endif // GAMEFallout4VR_H diff --git a/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.json b/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/fallout4vr/splash.png b/libs/game_bethesda/src/games/fallout4vr/splash.png new file mode 100644 index 0000000..b5908bc Binary files /dev/null and b/libs/game_bethesda/src/games/fallout4vr/splash.png differ diff --git a/libs/game_bethesda/src/games/fallout76/CMakeLists.txt b/libs/game_bethesda/src/games/fallout76/CMakeLists.txt new file mode 100644 index 0000000..f7abcb2 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_fallout76 SHARED + fallout76dataarchives.cpp + fallout76dataarchives.h + fallout76moddatachecker.h + fallout76moddatacontent.h + fallout76savegame.cpp + fallout76savegame.h + fallout76savegameinfo.cpp + fallout76savegameinfo.h + fallout76scriptextender.cpp + fallout76scriptextender.h + fallout76unmanagedmods.cpp + fallout76unmanagedmods.h + gamefallout76.cpp + gamefallout76.h +) +mo2_configure_plugin(game_fallout76 NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_fallout76 PRIVATE game_creation) +mo2_install_plugin(game_fallout76) diff --git a/libs/game_bethesda/src/games/fallout76/fallout76.qrc b/libs/game_bethesda/src/games/fallout76/fallout76.qrc new file mode 100644 index 0000000..de54646 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76.qrc @@ -0,0 +1,5 @@ + + + splash.png + + diff --git a/libs/game_bethesda/src/games/fallout76/fallout76dataarchives.cpp b/libs/game_bethesda/src/games/fallout76/fallout76dataarchives.cpp new file mode 100644 index 0000000..6827d08 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76dataarchives.cpp @@ -0,0 +1,150 @@ +#include "fallout76dataarchives.h" + +#include "iprofile.h" +#include + +#include + +QStringList Fallout76DataArchives::vanillaArchives() const +{ + return {"SeventySix - Animations.ba2", + "SeventySix - ATX_Main.ba2", + "SeventySix - ATX_Textures.ba2", + "SeventySix - EnlightenExteriors01.ba2", + "SeventySix - EnlightenExteriors02.ba2", + "SeventySix - EnlightenInteriors.ba2", + "SeventySix - GeneratedMeshes.ba2", + "SeventySix - GeneratedTextures.ba2", + "SeventySix - Interface.ba2", + "SeventySix - Localization.ba2", + "SeventySix - Materials.ba2", + "SeventySix - Meshes01.ba2", + "SeventySix - Meshes02.ba2", + "SeventySix - MeshesExtra.ba2", + "SeventySix - MiscClient.ba2", + "SeventySix - Shaders.ba2", + "SeventySix - Sounds01.ba2", + "SeventySix - Sounds02.ba2", + "SeventySix - Startup.ba2", + "SeventySix - Textures01.ba2", + "SeventySix - Textures02.ba2", + "SeventySix - Textures03.ba2", + "SeventySix - Textures04.ba2", + "SeventySix - Textures05.ba2", + "SeventySix - Textures06.ba2", + "SeventySix - Voices.ba2"}; +} + +QStringList Fallout76DataArchives::sResourceIndexFileList() const +{ + return {"SeventySix - Textures01.ba2", "SeventySix - Textures02.ba2", + "SeventySix - Textures03.ba2", "SeventySix - Textures04.ba2", + "SeventySix - Textures05.ba2", "SeventySix - Textures06.ba2"}; +} + +QStringList Fallout76DataArchives::sResourceStartUpArchiveList() const +{ + return {"SeventySix - Interface.ba2", "SeventySix - Localization.ba2", + "SeventySix - Shaders.ba2", "SeventySix - Startup.ba2"}; +} + +QStringList Fallout76DataArchives::SResourceArchiveMemoryCacheList() const +{ + return {"SeventySix - Interface.ba2", "SeventySix - Materials.ba2", + "SeventySix - MiscClient.ba2", "SeventySix - Shaders.ba2"}; +} + +QStringList Fallout76DataArchives::SResourceArchiveList() const +{ + return {"SeventySix - GeneratedMeshes.ba2", "SeventySix - Materials.ba2", + "SeventySix - Meshes01.ba2", "SeventySix - Meshes02.ba2", + "SeventySix - MeshesExtra.ba2", "SeventySix - MiscClient.ba2", + "SeventySix - Sounds01.ba2", "SeventySix - Sounds02.ba2", + "SeventySix - Startup.ba2", "SeventySix - Voices.ba2"}; +} + +QStringList Fallout76DataArchives::SResourceArchiveList2() const +{ + return {"SeventySix - Animations.ba2", "SeventySix - EnlightenInteriors.ba2", + "SeventySix - GeneratedTextures.ba2", "SeventySix - EnlightenExteriors01.ba2", + "SeventySix - EnlightenExteriors02.ba2"}; +} + +QStringList Fallout76DataArchives::sResourceArchive2List() const +{ + return {"SeventySix - ATX_Main.ba2", "SeventySix - ATX_Textures.ba2"}; +} + +QStringList Fallout76DataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = + profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("Fallout76.ini") + : localGameDirectory().absoluteFilePath("Fallout76.ini"); + + result.append(getArchivesFromKey(iniFile, "sResourceIndexFileList")); + result.append(getArchivesFromKey(iniFile, "sResourceStartUpArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveMemoryCacheList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + result.append(getArchivesFromKey(iniFile, "sResourceArchive2List")); + + return result; +} + +void Fallout76DataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString iniFile = + profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("Fallout76.ini") + : localGameDirectory().absoluteFilePath("Fallout76.ini"); + + QStringList sResourceIndexFileList = {}; + QStringList sResourceStartUpArchiveList = {}; + QStringList SResourceArchiveMemoryCacheList = {}; + QStringList SResourceArchiveList = {}; + QStringList SResourceArchiveList2 = {}; + QStringList sResourceArchive2List = {}; + + for (int i = 0; i < before.size(); ++i) { + QString archive = before[i]; + if (archive.contains(QRegularExpression(" - Textures(\\d{2})\\.ba2$"))) { + sResourceIndexFileList.append(archive); + } else if (archive.contains(QRegularExpression( + " - (Interface|Localization|Shaders|Startup)\\.ba2$"))) { + sResourceStartUpArchiveList.append(archive); + } else if (archive.contains(QRegularExpression( + " - (Interface|Materials|MiscClient|Shaders)\\.ba2$"))) { + SResourceArchiveMemoryCacheList.append(archive); + } else if (archive.contains(QRegularExpression( + " - " + "(GeneratedMeshes|Materials|Meshes(\\d{2}|\\w+)?|MiscClient|" + "Sounds\\d{2}|Startup|Voices)\\.ba2$"))) { + SResourceArchiveList.append(archive); + } else if (archive.contains( + QRegularExpression(" - " + "(Animations|Enlighten(Interiors|Exteriors\\d{2})" + "|GeneratedTextures)\\.ba2$"))) { + SResourceArchiveList2.append(archive); + } else if (archive.contains(QRegularExpression(" - ATX_.*\\.ba2$"))) { + // if it is named after DLC, it has to go here + sResourceArchive2List.append(archive); + } else { + // if it did not fit any description above, it gets tacked on at the end + sResourceArchive2List.append(archive); + } + } + + setArchivesToKey(iniFile, "sResourceIndexFileList", + sResourceIndexFileList.join(", ")); + setArchivesToKey(iniFile, "sResourceStartUpArchiveList", + sResourceStartUpArchiveList.join(", ")); + setArchivesToKey(iniFile, "SResourceArchiveMemoryCacheList", + SResourceArchiveMemoryCacheList.join(", ")); + setArchivesToKey(iniFile, "SResourceArchiveList", SResourceArchiveList.join(", ")); + setArchivesToKey(iniFile, "SResourceArchiveList2", SResourceArchiveList2.join(", ")); + setArchivesToKey(iniFile, "sResourceArchive2List", sResourceArchive2List.join(", ")); +} diff --git a/libs/game_bethesda/src/games/fallout76/fallout76dataarchives.h b/libs/game_bethesda/src/games/fallout76/fallout76dataarchives.h new file mode 100644 index 0000000..1cf9119 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76dataarchives.h @@ -0,0 +1,33 @@ +#ifndef FALLOUT76DATAARCHIVES_H +#define FALLOUT76DATAARCHIVES_H + +#include "gamebryodataarchives.h" + +namespace MOBase +{ +class IProfile; +} + +#include +#include + +class Fallout76DataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList sResourceIndexFileList() const; + virtual QStringList sResourceStartUpArchiveList() const; + virtual QStringList SResourceArchiveMemoryCacheList() const; + virtual QStringList SResourceArchiveList() const; + virtual QStringList SResourceArchiveList2() const; + virtual QStringList sResourceArchive2List() const; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // FALLOUT76DATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/fallout76/fallout76moddatachecker.h b/libs/game_bethesda/src/games/fallout76/fallout76moddatachecker.h new file mode 100644 index 0000000..26acc2a --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76moddatachecker.h @@ -0,0 +1,44 @@ +#ifndef FALLOUT76_MODATACHECKER_H +#define FALLOUT76_MODATACHECKER_H + +#include + +class Fallout76ModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{"interface", + "meshes", + "music", + "scripts", + "sound", + "strings", + "textures", + "trees", + "video", + "materials", + "distantlod", + "asi", + "Tools", + "MCM", + "distantland", + "mits", + "dllplugins", + "CalienteTools", + "NetScriptFramework", + "shadersfx", + "aaf"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "esl", "ba2", "modgroups"}; + return result; + } +}; + +#endif // FALLOUT76_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/fallout76/fallout76moddatacontent.h b/libs/game_bethesda/src/games/fallout76/fallout76moddatacontent.h new file mode 100644 index 0000000..df9f575 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76moddatacontent.h @@ -0,0 +1,44 @@ +#ifndef FALLOUT76_MODDATACONTENT_H +#define FALLOUT76_MODDATACONTENT_H + +#include +#include + +class Fallout76ModDataContent : public GamebryoModDataContent +{ +protected: + enum Fallout76Content + { + CONTENT_MATERIAL = CONTENT_NEXT_VALUE + }; + +public: + Fallout76ModDataContent(MOBase::IGameFeatures const* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + m_Enabled[CONTENT_SKYPROC] = false; + } + + std::vector getAllContents() const override + { + auto contents = GamebryoModDataContent::getAllContents(); + contents.push_back( + Content(CONTENT_MATERIAL, "Materials", ":/MO/gui/content/material")); + return contents; + } + + std::vector + getContentsFor(std::shared_ptr fileTree) const override + { + auto contents = GamebryoModDataContent::getContentsFor(fileTree); + for (auto e : *fileTree) { + if (e->compare("materials") == 0) { + contents.push_back(CONTENT_MATERIAL); + break; // Early break if you have nothing else to check. + } + } + return contents; + } +}; + +#endif // FALLOUT76_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/fallout76/fallout76savegame.cpp b/libs/game_bethesda/src/games/fallout76/fallout76savegame.cpp new file mode 100644 index 0000000..66e8a00 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76savegame.cpp @@ -0,0 +1,106 @@ +#include "fallout76savegame.h" + +#ifdef _WIN32 +#include +#else +#include + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gamefallout76.h" + +Fallout76SaveGame::Fallout76SaveGame(QString const& fileName, GameFallout76 const* game) + : GamebryoSaveGame(fileName, game, true) +{ + FileWrapper file(fileName, "FO76_SAVEGAME"); + + FILETIME ftime; + fetchInformationFields(file, m_PCName, m_PCLevel, m_PCLocation, m_SaveNumber, ftime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&ftime, &ctime); + + setCreationTime(ctime); +} + +void Fallout76SaveGame::fetchInformationFields(FileWrapper& file, QString playerName, + unsigned short playerLevel, + QString playerLocation, + unsigned long saveNumber, + FILETIME& creationTime) const +{ + + file.skip(); // header size + file.skip(); // header version + file.read(saveNumber); + + file.read(playerName); + + unsigned long temp; + file.read(temp); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString ignore; + file.read(ignore); // playtime as ascii hh.mm.ss + file.read(ignore); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + FILETIME ftime; + file.read(ftime); +} + +std::unique_ptr Fallout76SaveGame::fetchDataFields() const +{ + + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); // 10bytes + { + + FILETIME ftime; + fetchInformationFields(file, m_PCName, m_PCLevel, m_PCLocation, m_SaveNumber, + ftime); + } + + std::unique_ptr fields = std::make_unique(); + + file.readImage(384, true); + + uint8_t saveGameVersion = file.readChar(); + QString ignore; + file.read(ignore); // game version + file.skip(); // plugin info size + + file.readPlugins(); + if (saveGameVersion >= 68) { + file.readLightPlugins(); + } + + return fields; +} diff --git a/libs/game_bethesda/src/games/fallout76/fallout76savegame.h b/libs/game_bethesda/src/games/fallout76/fallout76savegame.h new file mode 100644 index 0000000..48a8666 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76savegame.h @@ -0,0 +1,23 @@ +#ifndef FALLOUT76SAVEGAME_H +#define FALLOUT76SAVEGAME_H + +#include "gamebryosavegame.h" +#include "windows_compat.h" + +class GameFallout76; + +class Fallout76SaveGame : public GamebryoSaveGame +{ +public: + Fallout76SaveGame(QString const& fileName, GameFallout76 const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& wrapper, QString playerName, + unsigned short playerLevel, QString playerLocation, + unsigned long saveNumber, FILETIME& creationTime) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // FALLOUT76SAVEGAME_H diff --git a/libs/game_bethesda/src/games/fallout76/fallout76savegameinfo.cpp b/libs/game_bethesda/src/games/fallout76/fallout76savegameinfo.cpp new file mode 100644 index 0000000..c6cb621 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76savegameinfo.cpp @@ -0,0 +1,10 @@ +#include "fallout76savegameinfo.h" + +#include "fallout76savegame.h" +#include "gamegamebryo.h" + +Fallout76SaveGameInfo::Fallout76SaveGameInfo(GameGamebryo const* game) + : GamebryoSaveGameInfo(game) +{} + +Fallout76SaveGameInfo::~Fallout76SaveGameInfo() {} diff --git a/libs/game_bethesda/src/games/fallout76/fallout76savegameinfo.h b/libs/game_bethesda/src/games/fallout76/fallout76savegameinfo.h new file mode 100644 index 0000000..457777c --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76savegameinfo.h @@ -0,0 +1,15 @@ +#ifndef FALLOUT76SAVEGAMEINFO_H +#define FALLOUT76SAVEGAMEINFO_H + +#include "gamebryosavegameinfo.h" + +class GameGamebryo; + +class Fallout76SaveGameInfo : public GamebryoSaveGameInfo +{ +public: + Fallout76SaveGameInfo(GameGamebryo const* game); + ~Fallout76SaveGameInfo(); +}; + +#endif // FALLOUT76SAVEGAMEINFO_H diff --git a/libs/game_bethesda/src/games/fallout76/fallout76scriptextender.cpp b/libs/game_bethesda/src/games/fallout76/fallout76scriptextender.cpp new file mode 100644 index 0000000..8a46c5e --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76scriptextender.cpp @@ -0,0 +1,18 @@ +#include "fallout76scriptextender.h" + +#include +#include + +Fallout76ScriptExtender::Fallout76ScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString Fallout76ScriptExtender::BinaryName() const +{ + return "f76se_loader.exe"; +} + +QString Fallout76ScriptExtender::PluginPath() const +{ + return "f76se/plugins"; +} diff --git a/libs/game_bethesda/src/games/fallout76/fallout76scriptextender.h b/libs/game_bethesda/src/games/fallout76/fallout76scriptextender.h new file mode 100644 index 0000000..206d597 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76scriptextender.h @@ -0,0 +1,17 @@ +#ifndef FALLOUT76SCRIPTEXTENDER_H +#define FALLOUT76SCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class Fallout76ScriptExtender : public GamebryoScriptExtender +{ +public: + Fallout76ScriptExtender(GameGamebryo const* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // FALLOUT76SCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/fallout76/fallout76unmanagedmods.cpp b/libs/game_bethesda/src/games/fallout76/fallout76unmanagedmods.cpp new file mode 100644 index 0000000..1ddc21c --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76unmanagedmods.cpp @@ -0,0 +1,46 @@ +#include "fallout76unmanagedmods.h" + +Fallout76UnmangedMods::Fallout76UnmangedMods(const GameGamebryo* game) + : GamebryoUnmangedMods(game) +{} + +Fallout76UnmangedMods::~Fallout76UnmangedMods() {} + +QStringList Fallout76UnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QDir dataDir(game()->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + QFileInfo file(fileName); + result.append(file.baseName()); + } + } + } + + return result; +} + +QStringList Fallout76UnmangedMods::secondaryFiles(const QString& modName) const +{ + // file extension in FO76 is .ba2 instead of bsa + QStringList archives; + QDir dataDir = game()->dataDirectory(); + for (const QString& archiveName : dataDir.entryList({modName + "*.ba2"})) { + archives.append(dataDir.absoluteFilePath(archiveName)); + } + return archives; +} + +QString Fallout76UnmangedMods::displayName(const QString& modName) const +{ + return modName; +} diff --git a/libs/game_bethesda/src/games/fallout76/fallout76unmanagedmods.h b/libs/game_bethesda/src/games/fallout76/fallout76unmanagedmods.h new file mode 100644 index 0000000..17d476d --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76unmanagedmods.h @@ -0,0 +1,18 @@ +#ifndef FALLOUT76UNMANAGEDMODS_H +#define FALLOUT76UNMANAGEDMODS_H + +#include "gamebryounmanagedmods.h" +#include + +class Fallout76UnmangedMods : public GamebryoUnmangedMods +{ +public: + Fallout76UnmangedMods(const GameGamebryo* game); + ~Fallout76UnmangedMods(); + + virtual QStringList mods(bool onlyOfficial) const override; + virtual QStringList secondaryFiles(const QString& modName) const override; + virtual QString displayName(const QString& modName) const override; +}; + +#endif // FALLOUT76UNMANAGEDMODS_H diff --git a/libs/game_bethesda/src/games/fallout76/game_fallout76_en.ts b/libs/game_bethesda/src/games/fallout76/game_fallout76_en.ts new file mode 100644 index 0000000..0fc8da5 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/game_fallout76_en.ts @@ -0,0 +1,13 @@ + + + + + GameFallout76 + + + Adds support for the game Fallout 76. +Splash by %1 + + + + diff --git a/libs/game_bethesda/src/games/fallout76/gamefallout76.cpp b/libs/game_bethesda/src/games/fallout76/gamefallout76.cpp new file mode 100644 index 0000000..bfeb507 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/gamefallout76.cpp @@ -0,0 +1,227 @@ +#include "gamefallout76.h" + +#include "fallout76dataarchives.h" +#include "fallout76moddatachecker.h" +#include "fallout76moddatacontent.h" +#include "fallout76unmanagedmods.h" + +#include "versioninfo.h" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "scopeguard.h" + +using namespace MOBase; + +GameFallout76::GameFallout76() {} + +bool GameFallout76::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameFallout76::gameName() const +{ + return "Fallout 76"; +} + +void GameFallout76::detectGame() +{ + m_GamePath = identifyGamePath(); + m_MyGamesPath = determineMyGamesPath(gameName()); +} + +QList GameFallout76::executables() const +{ + return QList() + << ExecutableInfo("Fallout 76", findInGameFolder(binaryName())); +} + +QList GameFallout76::executableForcedLoads() const +{ + return {}; +} + +QString GameFallout76::name() const +{ + return "Fallout 76 Support Plugin"; +} + +QString GameFallout76::author() const +{ + return "Mod Organizer Team; EntranceJew"; +} + +QString GameFallout76::description() const +{ + return tr("Adds support for the game Fallout 76.\n" + "Splash by %1") + .arg("nekoyoubi"); +} + +MOBase::VersionInfo GameFallout76::version() const +{ + return VersionInfo(1, 0, 0, VersionInfo::RELEASE_FINAL); +} + +QList GameFallout76::settings() const +{ + return QList(); +} + +void GameFallout76::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Fallout76", path, "plugins.txt"); + copyToProfile(localAppFolder() + "/Fallout76", path, "loadorder.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/Fallout76.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "Fallout76_default.ini", + "Fallout76.ini"); + } else { + copyToProfile(myGamesPath(), path, "Fallout76.ini"); + } + + copyToProfile(myGamesPath(), path, "Fallout76Prefs.ini"); + copyToProfile(myGamesPath(), path, "Fallout76Custom.ini"); + } +} + +QString GameFallout76::savegameExtension() const +{ + return ""; +} + +QString GameFallout76::savegameSEExtension() const +{ + return ""; +} + +std::vector> +GameFallout76::listSaves(QDir folder) const +{ + return {}; +} + +QString GameFallout76::identifyGamePath() const +{ + return parseSteamLocation(steamAPPId(), gameShortName()); +} + +std::shared_ptr GameFallout76::makeSaveGame(QString) const +{ + return nullptr; +} + +QString GameFallout76::steamAPPId() const +{ + return "1151340"; +} + +QStringList GameFallout76::primaryPlugins() const +{ + QStringList plugins = {"SeventySix.esm"}; + + plugins.append(CCPlugins()); + + return plugins; +} + +QStringList GameFallout76::gameVariants() const +{ + return {"Regular"}; +} + +QString GameFallout76::gameShortName() const +{ + return "Fallout76"; +} + +QString GameFallout76::gameNexusName() const +{ + return "Fallout76"; +} + +QStringList GameFallout76::iniFiles() const +{ + return {"Fallout76.ini", "Fallout76Prefs.ini", "Fallout76Custom.ini"}; +} + +QStringList GameFallout76::DLCPlugins() const +{ + return {}; +} + +QStringList GameFallout76::CCPlugins() const +{ + QStringList plugins = {}; + QFile file(gameDirectory().absoluteFilePath("Fallout76.ccc")); + if (file.open(QIODevice::ReadOnly)) { + ON_BLOCK_EXIT([&file]() { + file.close(); + }); + + if (file.size() == 0) { + return plugins; + } + while (!file.atEnd()) { + QByteArray line = file.readLine().trimmed(); + QString modName; + if ((line.size() > 0) && (line.at(0) != '#')) { + modName = QString::fromUtf8(line.constData()).toLower(); + } + + if (modName.size() > 0) { + if (!plugins.contains(modName, Qt::CaseInsensitive)) { + plugins.append(modName); + } + } + } + } + return plugins; +} + +IPluginGame::SortMechanism GameFallout76::sortMechanism() const +{ + return IPluginGame::SortMechanism::NONE; +} + +IPluginGame::LoadOrderMechanism GameFallout76::loadOrderMechanism() const +{ + return IPluginGame::LoadOrderMechanism::PluginsTxt; +} + +int GameFallout76::nexusModOrganizerID() const +{ + return -1; +} + +int GameFallout76::nexusGameID() const +{ + return 2590; +} diff --git a/libs/game_bethesda/src/games/fallout76/gamefallout76.h b/libs/game_bethesda/src/games/fallout76/gamefallout76.h new file mode 100644 index 0000000..418ad23 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/gamefallout76.h @@ -0,0 +1,55 @@ +#ifndef GAMEFALLOUT76_H +#define GAMEFALLOUT76_H + +#include "gamegamebryo.h" + +#include +#include + +class GameFallout76 : public GameGamebryo +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "in.ejew.GameFallout76" FILE "gamefallout76.json") + +public: + GameFallout76(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + QString gameName() const override; + void detectGame() override; + QList executables() const override; + QList executableForcedLoads() const override; + void initializeProfile(const QDir& path, ProfileSettings settings) const override; + QString steamAPPId() const override; + QStringList primaryPlugins() const override; + QStringList gameVariants() const override; + QString gameShortName() const override; + QString gameNexusName() const override; + QStringList iniFiles() const override; + QStringList DLCPlugins() const override; + QStringList CCPlugins() const override; + SortMechanism sortMechanism() const override; + LoadOrderMechanism loadOrderMechanism() const override; + int nexusModOrganizerID() const override; + int nexusGameID() const override; + std::vector> + listSaves(QDir folder) const override; + +public: // IPlugin interface + QString name() const override; + QString author() const override; + QString description() const override; + MOBase::VersionInfo version() const override; + QList settings() const override; + +protected: + QString identifyGamePath() const override; + std::shared_ptr makeSaveGame(QString) const override; + QString savegameExtension() const override; + QString savegameSEExtension() const override; +}; + +#endif // GAMEFallout76_H diff --git a/libs/game_bethesda/src/games/fallout76/gamefallout76.json b/libs/game_bethesda/src/games/fallout76/gamefallout76.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/gamefallout76.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/fallout76/splash.png b/libs/game_bethesda/src/games/fallout76/splash.png new file mode 100644 index 0000000..d612e7b Binary files /dev/null and b/libs/game_bethesda/src/games/fallout76/splash.png differ diff --git a/libs/game_bethesda/src/games/falloutnv/CMakeLists.txt b/libs/game_bethesda/src/games/falloutnv/CMakeLists.txt new file mode 100644 index 0000000..4b7a556 --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_falloutnv SHARED + falloutnvbsainvalidation.cpp + falloutnvbsainvalidation.h + falloutnvdataarchives.cpp + falloutnvdataarchives.h + falloutnvmoddatachecker.h + falloutnvmoddatacontent.h + falloutnvsavegame.cpp + falloutnvsavegame.h + falloutnvscriptextender.cpp + falloutnvscriptextender.h + gamefalloutnv.cpp + gamefalloutnv.h +) +mo2_configure_plugin(game_falloutnv NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_falloutnv PRIVATE game_gamebryo) +set_target_properties(game_falloutnv PROPERTIES OUTPUT_NAME "game_falloutNV") +mo2_install_plugin(game_falloutnv) + +if(NOT WIN32) + add_custom_command(TARGET game_falloutnv POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/src/src/plugins" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" + "${CMAKE_BINARY_DIR}/src/src/plugins/libgame_falloutNV.so" + COMMAND ${CMAKE_COMMAND} -E rm -f + "${CMAKE_BINARY_DIR}/src/src/plugins/libgame_falloutnv.so") +endif() diff --git a/libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.cpp b/libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.cpp new file mode 100644 index 0000000..d66591f --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "falloutnvbsainvalidation.h" + +FalloutNVBSAInvalidation::FalloutNVBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "fallout.ini", game) +{} + +QString FalloutNVBSAInvalidation::invalidationBSAName() const +{ + return "Fallout - Invalidation.bsa"; +} + +unsigned long FalloutNVBSAInvalidation::bsaVersion() const +{ + return 0x68; +} diff --git a/libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.h b/libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.h new file mode 100644 index 0000000..2301acf --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.h @@ -0,0 +1,20 @@ +#ifndef FALLOUTNVBSAINVALIDATION_H +#define FALLOUTNVBSAINVALIDATION_H + +#include "falloutnvdataarchives.h" +#include "gamebryobsainvalidation.h" + +#include + +class FalloutNVBSAInvalidation : public GamebryoBSAInvalidation +{ +public: + FalloutNVBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; +}; + +#endif // FALLOUTNVBSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.cpp b/libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.cpp new file mode 100644 index 0000000..7314561 --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.cpp @@ -0,0 +1,32 @@ +#include "falloutnvdataarchives.h" +#include + +QStringList FalloutNVDataArchives::vanillaArchives() const +{ + return {"Fallout - Textures.bsa", "Fallout - Textures2.bsa", "Fallout - Meshes.bsa", + "Fallout - Voices1.bsa", "Fallout - Sound.bsa", "Fallout - Misc.bsa"}; +} + +QStringList FalloutNVDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini") + : localGameDirectory().absoluteFilePath("fallout.ini"); + result.append(getArchivesFromKey(iniFile, "SArchiveList", + 8192)); // NVAC expands the maximum string limit + + return result; +} + +void FalloutNVDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini") + : localGameDirectory().absoluteFilePath("fallout.ini"); + setArchivesToKey(iniFile, "SArchiveList", list); +} diff --git a/libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.h b/libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.h new file mode 100644 index 0000000..07e64a1 --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.h @@ -0,0 +1,23 @@ +#ifndef FALLOUTNVDATAARCHIVES_H +#define FALLOUTNVDATAARCHIVES_H + +#include +#include +#include +#include +#include + +class FalloutNVDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // FALLOUTNVDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/falloutnv/falloutnvmoddatachecker.h b/libs/game_bethesda/src/games/falloutnv/falloutnvmoddatachecker.h new file mode 100644 index 0000000..02c5c7f --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/falloutnvmoddatachecker.h @@ -0,0 +1,51 @@ +#ifndef FALLOUTNV_MODATACHECKER_H +#define FALLOUTNV_MODATACHECKER_H + +#include + +class FalloutNVModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{"fonts", + "interface", + "menus", + "meshes", + "music", + "scripts", + "shaders", + "sound", + "strings", + "textures", + "trees", + "video", + "facegen", + "materials", + "nvse", + "distantlod", + "asi", + "Tools", + "MCM", + "distantland", + "mits", + "dllplugins", + "CalienteTools", + "shadersfx", + "config", + "KEYWORDS", + "BaseObjectSwapper", + "RaceMenuPresets"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "bsa", "modgroups", "ini"}; + return result; + } +}; + +#endif // FALLOUTNV_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/falloutnv/falloutnvmoddatacontent.h b/libs/game_bethesda/src/games/falloutnv/falloutnvmoddatacontent.h new file mode 100644 index 0000000..4effd97 --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/falloutnvmoddatacontent.h @@ -0,0 +1,22 @@ +#ifndef FALLOUTNV_MODDATACONTENT_H +#define FALLOUTNV_MODDATACONTENT_H + +#include +#include + +class FalloutNVModDataContent : public GamebryoModDataContent +{ +public: + /** + * + */ + FalloutNVModDataContent(MOBase::IGameFeatures const* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + // Just need to disable some contents: + m_Enabled[CONTENT_MCM] = false; + m_Enabled[CONTENT_SKYPROC] = false; + } +}; + +#endif // FALLOUTNV_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/falloutnv/falloutnvsavegame.cpp b/libs/game_bethesda/src/games/falloutnv/falloutnvsavegame.cpp new file mode 100644 index 0000000..e6bbc2d --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/falloutnvsavegame.cpp @@ -0,0 +1,77 @@ +#include "falloutnvsavegame.h" + +#include "gamefalloutnv.h" + +FalloutNVSaveGame::FalloutNVSaveGame(QString const& fileName, GameFalloutNV const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "FO3SAVEGAME"); + uint32_t width, height; + fetchInformationFields(file, width, height, m_SaveNumber, m_PCName, m_PCLevel, + m_PCLocation); +} + +void FalloutNVSaveGame::fetchInformationFields(FileWrapper& file, uint32_t& width, + uint32_t& height, + uint32_t& saveNumber, + QString& playerName, + unsigned short& playerLevel, + QString& playerLocation) const +{ + file.skip(); // Save header size + + file.skip(); // File version? + file.skip(); // Delimiter + + // A huge wodge of text with no length but a delimiter. Given the null bytes + // in it I presume it's fixed length (64 bytes + delim) but I have no + // definite spec + for (unsigned char ignore = 0; ignore != 0x7c;) { + file.read(ignore); // unknown + } + + file.setHasFieldMarkers(true); + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BZSTRING); + + file.read(width); + file.read(height); + file.read(saveNumber); + file.read(playerName); + + QString whatthis; + file.read(whatthis); + + long level; + file.read(level); + playerLevel = level; + file.read(playerLocation); +} + +std::unique_ptr FalloutNVSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "FO3SAVEGAME"); + + std::unique_ptr fields = std::make_unique(); + + uint32_t width, height; + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + + fetchInformationFields(file, width, height, dummySaveNumber, dummyName, dummyLevel, + dummyLocation); + } + + QString playtime; + file.read(playtime); + + fields->Screenshot = file.readImage(width, height, 256); + + file.skip(5); // unknown (1 byte), plugin size (4 bytes) + + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING); + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/falloutnv/falloutnvsavegame.h b/libs/game_bethesda/src/games/falloutnv/falloutnvsavegame.h new file mode 100644 index 0000000..191deaf --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/falloutnvsavegame.h @@ -0,0 +1,23 @@ +#ifndef FALLOUTNVSAVEGAME_H +#define FALLOUTNVSAVEGAME_H + +#include "gamebryosavegame.h" + +class GameFalloutNV; + +class FalloutNVSaveGame : public GamebryoSaveGame +{ +public: + FalloutNVSaveGame(QString const& fileName, GameFalloutNV const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& wrapper, uint32_t& width, + uint32_t& height, uint32_t& saveNumber, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // FALLOUTNVSAVEGAME_H diff --git a/libs/game_bethesda/src/games/falloutnv/falloutnvscriptextender.cpp b/libs/game_bethesda/src/games/falloutnv/falloutnvscriptextender.cpp new file mode 100644 index 0000000..fc00c31 --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/falloutnvscriptextender.cpp @@ -0,0 +1,18 @@ +#include "falloutnvscriptextender.h" + +#include +#include + +FalloutNVScriptExtender::FalloutNVScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString FalloutNVScriptExtender::BinaryName() const +{ + return "nvse_loader.exe"; +} + +QString FalloutNVScriptExtender::PluginPath() const +{ + return "nvse/plugins"; +} diff --git a/libs/game_bethesda/src/games/falloutnv/falloutnvscriptextender.h b/libs/game_bethesda/src/games/falloutnv/falloutnvscriptextender.h new file mode 100644 index 0000000..9db160f --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/falloutnvscriptextender.h @@ -0,0 +1,17 @@ +#ifndef FALLOUTNVSCRIPTEXTENDER_H +#define FALLOUTNVSCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class FalloutNVScriptExtender : public GamebryoScriptExtender +{ +public: + FalloutNVScriptExtender(const GameGamebryo* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // FALLOUTNVSCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/falloutnv/game_falloutNV_en.ts b/libs/game_bethesda/src/games/falloutnv/game_falloutNV_en.ts new file mode 100644 index 0000000..8aa62ec --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/game_falloutNV_en.ts @@ -0,0 +1,22 @@ + + + + + GameFalloutNV + + + Fallout NV Support Plugin + + + + + Adds support for the game Fallout New Vegas + + + + + While not recommended by the FNV modding community, enables LOOT sorting + + + + diff --git a/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp b/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp new file mode 100644 index 0000000..1e8e9e9 --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp @@ -0,0 +1,334 @@ +#include "gamefalloutnv.h" + +#include "falloutnvbsainvalidation.h" +#include "falloutnvdataarchives.h" +#include "falloutnvmoddatachecker.h" +#include "falloutnvmoddatacontent.h" +#include "falloutnvsavegame.h" +#include "falloutnvscriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" +#include "versioninfo.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace MOBase; + +GameFalloutNV::GameFalloutNV() {} + +bool GameFalloutNV::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "fallout.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +void GameFalloutNV::setVariant(QString variant) +{ + m_GameVariant = variant; +} + +void GameFalloutNV::checkVariants() +{ + QFileInfo gog_dll(m_GamePath + "\\Galaxy.dll"); + QFileInfo epic_dll(m_GamePath + "\\EOSSDK-Win32-Shipping.dll"); + if (gog_dll.exists()) + setVariant("GOG"); + else if (epic_dll.exists()) + setVariant("Epic Games"); + else + setVariant("Steam"); +} + +QDir GameFalloutNV::documentsDirectory() const +{ + return m_MyGamesPath; +} + +QString GameFalloutNV::identifyGamePath() const +{ + auto result = GameGamebryo::identifyGamePath(); // Default registry path + // EPIC Game Store + if (result.isEmpty()) { + /** + * Basegame: 5daeb974a22a435988892319b3a4f476 + * Dead Money: b290229eb58045cbab9501640f3278f3 + * Honest Hearts: 562d4a2c1b3147b089a7c453e3ddbcbe + * Old World Blues: c8dae1ab0570475a8b38a9041e614840 + * Lonesome Road: 4fa3d8d9b2cb4714a19a38d1a598be8f + * Gun Runners' Arsenal: 7dcfb9cd9d134728b2646466c34c7b3b + * Courier's Stash: ee9a44b4530942499ef1c8c390731fce + */ + result = parseEpicGamesLocation({"5daeb974a22a435988892319b3a4f476"}); + if (QFileInfo(result).isDir()) { + QDir startPath = QDir(result); + auto subDirs = startPath.entryList({"Fallout New Vegas*"}, + QDir::Dirs | QDir::NoDotAndDotDot); + if (!subDirs.isEmpty()) + result = startPath.absoluteFilePath(subDirs.first()); + } + } + return result; +} + +void GameFalloutNV::setGamePath(const QString& path) +{ + m_GamePath = path; + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QDir GameFalloutNV::savesDirectory() const +{ + return QDir(m_MyGamesPath + "/Saves"); +} + +QString GameFalloutNV::myGamesPath() const +{ + return m_MyGamesPath; +} + +bool GameFalloutNV::isInstalled() const +{ + return !m_GamePath.isEmpty(); +} + +QString GameFalloutNV::gameName() const +{ + return "New Vegas"; +} + +QString GameFalloutNV::gameDirectoryName() const +{ + if (selectedVariant() == "Epic Games") + return "FalloutNV_Epic"; + else + return "FalloutNV"; +} + +void GameFalloutNV::detectGame() +{ + m_GamePath = identifyGamePath(); + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QList GameFalloutNV::executables() const +{ + ExecutableInfo game("New Vegas", findInGameFolder(binaryName())); + ExecutableInfo launcher("Fallout Launcher", findInGameFolder(getLauncherName())); + QList extraExecutables = + QList() << ExecutableInfo("Fallout Mod Manager", + findInGameFolder("fomm/fomm.exe")) + << ExecutableInfo("BOSS", + findInGameFolder("BOSS/BOSS.exe")) + << ExecutableInfo("GECK", findInGameFolder("geck.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"FalloutNV\""); + if (selectedVariant() != "Epic Games") { + extraExecutables.prepend(ExecutableInfo( + "NVSE", findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName()))); + } else { + game.withArgument("-EpicPortal"); + launcher.withArgument("-EpicPortal"); + } + QList executables = {game, launcher}; + executables += extraExecutables; + return executables; +} + +QList GameFalloutNV::executableForcedLoads() const +{ + return QList(); +} + +QString GameFalloutNV::name() const +{ + return "Fallout NV Support Plugin"; +} + +QString GameFalloutNV::localizedName() const +{ + return tr("Fallout NV Support Plugin"); +} + +QString GameFalloutNV::author() const +{ + return "Tannin & MO2 Team"; +} + +QString GameFalloutNV::description() const +{ + return tr("Adds support for the game Fallout New Vegas"); +} + +MOBase::VersionInfo GameFalloutNV::version() const +{ + return VersionInfo(1, 6, 0, VersionInfo::RELEASE_FINAL); +} + +QList GameFalloutNV::settings() const +{ + return QList() + << PluginSetting("enable_loot_sorting", + tr("While not recommended by the FNV modding community, " + "enables LOOT sorting"), + false); +} + +void GameFalloutNV::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/" + gameDirectoryName(), path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/fallout.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "fallout_default.ini", + "fallout.ini"); + } else { + copyToProfile(myGamesPath(), path, "fallout.ini"); + } + + copyToProfile(myGamesPath(), path, "falloutprefs.ini"); + copyToProfile(myGamesPath(), path, "falloutcustom.ini"); + copyToProfile(myGamesPath(), path, "GECKCustom.ini"); + copyToProfile(myGamesPath(), path, "GECKPrefs.ini"); + } +} + +QString GameFalloutNV::savegameExtension() const +{ + return "fos"; +} + +QString GameFalloutNV::savegameSEExtension() const +{ + return "nvse"; +} + +std::shared_ptr +GameFalloutNV::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameFalloutNV::steamAPPId() const +{ + if (selectedVariant() == "Steam") { + if (m_GamePath.endsWith("enplczru")) { + return "22490"; + } else { + return "22380"; + } + } + return QString(); +} + +QStringList GameFalloutNV::primaryPlugins() const +{ + return {"falloutnv.esm"}; +} + +QStringList GameFalloutNV::gameVariants() const +{ + return {"Steam", "GOG", "Epic Games"}; +} + +QString GameFalloutNV::gameShortName() const +{ + return "FalloutNV"; +} + +QStringList GameFalloutNV::validShortNames() const +{ + return {"Fallout3"}; +} + +QString GameFalloutNV::gameNexusName() const +{ + return "newvegas"; +} + +QStringList GameFalloutNV::iniFiles() const +{ + return {"fallout.ini", "falloutprefs.ini", "falloutcustom.ini", "GECKCustom.ini", + "GECKPrefs.ini"}; +} + +QStringList GameFalloutNV::DLCPlugins() const +{ + return {"DeadMoney.esm", "HonestHearts.esm", "OldWorldBlues.esm", + "LonesomeRoad.esm", "GunRunnersArsenal.esm", "CaravanPack.esm", + "ClassicPack.esm", "MercenaryPack.esm", "TribalPack.esm"}; +} + +MOBase::IPluginGame::SortMechanism GameFalloutNV::sortMechanism() const +{ + if (m_Organizer->pluginSetting(name(), "enable_loot_sorting").toBool()) + return IPluginGame::SortMechanism::LOOT; + return IPluginGame::SortMechanism::NONE; +} + +int GameFalloutNV::nexusModOrganizerID() const +{ + return 42572; +} + +int GameFalloutNV::nexusGameID() const +{ + return 130; +} + +QDir GameFalloutNV::gameDirectory() const +{ + return QDir(m_GamePath); +} + +MappingType GameFalloutNV::mappings() const +{ + MappingType result; + + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameShortName() + "/" + profileFile, + false}); + if (selectedVariant() == "Epic Games") { + result.push_back( + {m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameDirectoryName() + "/" + profileFile, false}); + } + } + + return result; +} diff --git a/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.h b/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.h new file mode 100644 index 0000000..74f98b2 --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.h @@ -0,0 +1,70 @@ +#ifndef GAMEFALLOUTNV_H +#define GAMEFALLOUTNV_H + +#include "gamegamebryo.h" + +#include +#include + +class GameFalloutNV : public GameGamebryo +{ + Q_OBJECT +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.GameFalloutNV" FILE "gamefalloutnv.json") +#endif + +public: + GameFalloutNV(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual void detectGame() override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QStringList gameVariants() const override; + virtual QString gameShortName() const override; + virtual QStringList validShortNames() const override; + virtual QString gameNexusName() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual SortMechanism sortMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + + virtual bool isInstalled() const override; + virtual void setGamePath(const QString& path) override; + virtual QDir gameDirectory() const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + virtual MappingType mappings() const override; + +protected: + QString gameDirectoryName() const; + QDir documentsDirectory() const; + QDir savesDirectory() const; + QString myGamesPath() const; + + void setVariant(QString variant); + void checkVariants(); + +protected: + virtual QString identifyGamePath() const override; + virtual QString savegameExtension() const override; + virtual QString savegameSEExtension() const override; + std::shared_ptr makeSaveGame(QString filePath) const override; +}; + +#endif // GAMEFALLOUTNV_H diff --git a/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.json b/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/morrowind/CMakeLists.txt b/libs/game_bethesda/src/games/morrowind/CMakeLists.txt new file mode 100644 index 0000000..5e92f4b --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_morrowind SHARED + gamemorrowind.cpp + gamemorrowind.h + morrowindbsainvalidation.cpp + morrowindbsainvalidation.h + morrowinddataarchives.cpp + morrowinddataarchives.h + morrowindgameplugins.cpp + morrowindgameplugins.h + morrowindlocalsavegames.cpp + morrowindlocalsavegames.h + morrowindmoddatachecker.h + morrowindmoddatacontent.h + morrowindsavegame.cpp + morrowindsavegame.h + morrowindsavegameinfo.cpp + morrowindsavegameinfo.h + morrowindsavegameinfowidget.cpp + morrowindsavegameinfowidget.h + morrowindsavegameinfowidget.ui + ) +mo2_configure_plugin(game_morrowind NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_morrowind PRIVATE game_gamebryo) +mo2_install_plugin(game_morrowind) diff --git a/libs/game_bethesda/src/games/morrowind/game_morrowind_en.ts b/libs/game_bethesda/src/games/morrowind/game_morrowind_en.ts new file mode 100644 index 0000000..c6961c4 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/game_morrowind_en.ts @@ -0,0 +1,69 @@ + + + + + GameMorrowind + + + Morrowind Support Plugin + + + + + Adds support for the game Morrowind. +Splash by %1 + + + + + MorrowindSaveGameInfoWidget + + + Save # + + + + + Character + + + + + Level + + + + + Health + + + + + Location + + + + + Date + + + + + Missing ESPs + + + + + None + + + + + QObject + + + Some of your plugins have invalid names! These plugins can not be loaded by the game. Please see mo_interface.log for a list of affected plugins and rename them. + + + + diff --git a/libs/game_bethesda/src/games/morrowind/gamemorrowind.cpp b/libs/game_bethesda/src/games/morrowind/gamemorrowind.cpp new file mode 100644 index 0000000..faa3a3b --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/gamemorrowind.cpp @@ -0,0 +1,216 @@ +#include "gamemorrowind.h" + +#include "morrowindbsainvalidation.h" +#include "morrowinddataarchives.h" +#include "morrowindgameplugins.h" +#include "morrowindlocalsavegames.h" +#include "morrowindmoddatachecker.h" +#include "morrowindmoddatacontent.h" +#include "morrowindsavegame.h" +#include "morrowindsavegameinfo.h" + +#include "executableinfo.h" +#include "pluginsetting.h" +#include "steamutility.h" + +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +using namespace MOBase; + +GameMorrowind::GameMorrowind() {} + +bool GameMorrowind::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(dataArchives); + registerFeature(std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameMorrowind::gameName() const +{ + return "Morrowind"; +} + +QString GameMorrowind::getLauncherName() const +{ + return "Morrowind Launcher.exe"; +} + +QDir GameMorrowind::dataDirectory() const +{ + return gameDirectory().absoluteFilePath("Data Files"); +} + +QDir GameMorrowind::savesDirectory() const +{ + return QDir(gameDirectory().absoluteFilePath("Saves")); +} + +QDir GameMorrowind::documentsDirectory() const +{ + return gameDirectory(); +} + +QList GameMorrowind::executables() const +{ + return QList() + << ExecutableInfo("MWSE (Launcher Method)", + findInGameFolder("MWSE Launcher.exe")) + << ExecutableInfo("Morrowind", findInGameFolder(binaryName())) + << ExecutableInfo("Morrowind Launcher", findInGameFolder(getLauncherName())) + << ExecutableInfo("MGE XE", findInGameFolder("MGEXEgui.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Morrowind\""); +} + +QList GameMorrowind::executableForcedLoads() const +{ + return QList(); +} + +QString GameMorrowind::name() const +{ + return "Morrowind Support Plugin"; +} + +QString GameMorrowind::localizedName() const +{ + return tr("Morrowind Support Plugin"); +} + +QString GameMorrowind::author() const +{ + return "Schilduin & MO2 Team"; +} + +QString GameMorrowind::description() const +{ + return tr("Adds support for the game Morrowind.\n" + "Splash by %1") + .arg("AnyOldName3"); +} + +MOBase::VersionInfo GameMorrowind::version() const +{ + return VersionInfo(1, 5, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameMorrowind::settings() const +{ + return QList(); +} + +void GameMorrowind::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Morrowind", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + copyToProfile(gameDirectory().absolutePath(), path, "Morrowind.ini"); + } +} + +QString GameMorrowind::savegameExtension() const +{ + return "ess"; +} + +QString GameMorrowind::savegameSEExtension() const +{ + return "mwse"; +} + +std::shared_ptr +GameMorrowind::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameMorrowind::steamAPPId() const +{ + return "22320"; +} + +QStringList GameMorrowind::primaryPlugins() const +{ + return {"morrowind.esm"}; +} + +QString GameMorrowind::binaryName() const +{ + return "Morrowind.exe"; +} + +QString GameMorrowind::gameShortName() const +{ + return "Morrowind"; +} + +QString GameMorrowind::gameNexusName() const +{ + return "Morrowind"; +} + +QStringList GameMorrowind::iniFiles() const +{ + return {"morrowind.ini"}; +} + +QStringList GameMorrowind::DLCPlugins() const +{ + return {"Tribunal.esm", "Bloodmoon.esm"}; +} + +MOBase::IPluginGame::SortMechanism GameMorrowind::sortMechanism() const +{ + return SortMechanism::LOOT; +} + +int GameMorrowind::nexusModOrganizerID() const +{ + return 1334; +} + +int GameMorrowind::nexusGameID() const +{ + return 100; +} + +QString GameMorrowind::identifyGamePath() const +{ + QString path = GameGamebryo::identifyGamePath(); + if (path.isEmpty()) { + // Resolve by Steam app id first (handles non-standard install directory names). + path = parseSteamLocation(steamAPPId(), ""); + } + if (path.isEmpty()) { + path = MOBase::findSteamGame("Morrowind", "Data Files/Morrowind.esm"); + } + return path; +} diff --git a/libs/game_bethesda/src/games/morrowind/gamemorrowind.h b/libs/game_bethesda/src/games/morrowind/gamemorrowind.h new file mode 100644 index 0000000..a6f6862 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/gamemorrowind.h @@ -0,0 +1,62 @@ +#ifndef GAMEMORROWIND_H +#define GAMEMORROWIND_H + +#include "gamegamebryo.h" + +#include +#include + +class GameMorrowind : public GameGamebryo +{ + Q_OBJECT +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "com.schilduin.GameMorrowind" FILE "gamemorrowind.json") +#endif + + friend class MorrowindSaveGameInfo; + friend class MorrowindSaveGameInfoWidget; + +public: + GameMorrowind(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual QString getLauncherName() const override; + virtual QDir dataDirectory() const override; + virtual QDir savesDirectory() const override; + virtual QDir documentsDirectory() const override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QString binaryName() const override; + virtual QString gameShortName() const override; + virtual QString gameNexusName() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual SortMechanism sortMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + virtual QString identifyGamePath() const; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + +protected: + virtual QString savegameExtension() const override; + virtual QString savegameSEExtension() const override; + virtual std::shared_ptr + makeSaveGame(QString filepath) const override; +}; + +#endif // GAMEMORROWIND_H diff --git a/libs/game_bethesda/src/games/morrowind/gamemorrowind.json b/libs/game_bethesda/src/games/morrowind/gamemorrowind.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/gamemorrowind.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/morrowind/morrowind.qrc b/libs/game_bethesda/src/games/morrowind/morrowind.qrc new file mode 100644 index 0000000..a459b56 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowind.qrc @@ -0,0 +1,5 @@ + + + splash.png + + diff --git a/libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.cpp b/libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.cpp new file mode 100644 index 0000000..9b63d4c --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "morrowindbsainvalidation.h" + +MorrowindBSAInvalidation::MorrowindBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "morrowind.ini", game) +{} + +QString MorrowindBSAInvalidation::invalidationBSAName() const +{ + return "Morrowind - Invalidation.bsa"; +} + +unsigned long MorrowindBSAInvalidation::bsaVersion() const +{ + return 0x68; +} diff --git a/libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.h b/libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.h new file mode 100644 index 0000000..8395abe --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.h @@ -0,0 +1,20 @@ +#ifndef MORROWINDBSAINVALIDATION_H +#define MORROWINDBSAINVALIDATION_H + +#include "gamebryobsainvalidation.h" +#include "morrowinddataarchives.h" + +#include + +class MorrowindBSAInvalidation : public GamebryoBSAInvalidation +{ +public: + MorrowindBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; +}; + +#endif // MORROWINDBSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/morrowind/morrowinddataarchives.cpp b/libs/game_bethesda/src/games/morrowind/morrowinddataarchives.cpp new file mode 100644 index 0000000..57a5944 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowinddataarchives.cpp @@ -0,0 +1,71 @@ +#include "morrowinddataarchives.h" +#include "registry.h" +#include + +#include + +#include "gamegamebryo.h" + +QStringList MorrowindDataArchives::vanillaArchives() const +{ + return {"Morrowind.bsa"}; +} + +QStringList MorrowindDataArchives::getArchives(const QString& iniFile) const +{ + QStringList result; + QSettings settings(iniFile, QSettings::IniFormat); + + QString key = "Archive "; + int i = 0; + while (true) { + QString value = + settings.value("Archives/" + key + QString::number(i), "").toString().trimmed(); + if (value.isEmpty()) { + break; + } + result.append(value); + i++; + } + + return result; +} + +void MorrowindDataArchives::setArchives(const QString& iniFile, const QStringList& list) +{ + QSettings settings(iniFile, QSettings::IniFormat); + settings.remove("Archives"); + + QString key = "Archive "; + int writtenCount = 0; + foreach (const QString& value, list) { + if (!MOBase::WriteRegistryValue( + "Archives", key + QString::number(writtenCount), value, iniFile)) { + qWarning("failed to set archives in \"%s\"", qUtf8Printable(iniFile)); + } + ++writtenCount; + } +} + +QStringList MorrowindDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = + profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("morrowind.ini") + : gameDirectory().absoluteFilePath("morrowind.ini"); + result.append(getArchives(iniFile)); + + return result; +} + +void MorrowindDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString iniFile = + profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("morrowind.ini") + : gameDirectory().absoluteFilePath("morrowind.ini"); + setArchives(iniFile, before); +} diff --git a/libs/game_bethesda/src/games/morrowind/morrowinddataarchives.h b/libs/game_bethesda/src/games/morrowind/morrowinddataarchives.h new file mode 100644 index 0000000..7ee0c1d --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowinddataarchives.h @@ -0,0 +1,28 @@ +#ifndef MORROWINDDATAARCHIVES_H +#define MORROWINDDATAARCHIVES_H + +#include +#include +#include +#include +#include +#include + +class MorrowindDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +protected: + QStringList getArchives(const QString& iniFile) const; + void setArchives(const QString& iniFile, const QStringList& list); + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // MORROWINDDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/morrowind/morrowindgameplugins.cpp b/libs/game_bethesda/src/games/morrowind/morrowindgameplugins.cpp new file mode 100644 index 0000000..27665a4 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowindgameplugins.cpp @@ -0,0 +1,197 @@ +#include "morrowindgameplugins.h" +#include "registry.h" +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using MOBase::IOrganizer; +using MOBase::IPluginList; +using MOBase::reportError; + +MorrowindGamePlugins::MorrowindGamePlugins(IOrganizer* organizer) + : GamebryoGamePlugins(organizer) +{} + +void MorrowindGamePlugins::writePluginLists(const IPluginList* pluginList) +{ + if (!m_LastRead.isValid()) { + // attempt to write uninitialized plugin lists + return; + } + + if (organizer()->profile()->localSettingsEnabled()) { + writePluginList(pluginList, + organizer()->profile()->absolutePath() + "/Morrowind.ini"); + } else { + writePluginList(pluginList, + organizer()->managedGame()->gameDirectory().absolutePath() + + "/Morrowind.ini"); + } + + writeLoadOrderList(pluginList, + organizer()->profile()->absolutePath() + "/loadorder.txt"); + + m_LastRead = QDateTime::currentDateTime(); +} + +void MorrowindGamePlugins::readPluginLists(MOBase::IPluginList* pluginList) +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + + QString pluginsPath = organizer()->profile()->absolutePath() + "/Morrowind.ini"; + if (!organizer()->profile()->localSettingsEnabled()) { + pluginsPath = + organizer()->managedGame()->gameDirectory().absolutePath() + "/Morrowind.ini"; + } + + bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || + QFileInfo(loadOrderPath).lastModified() > m_LastRead; + bool pluginsIsNew = + !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; + + if (loadOrderIsNew || !pluginsIsNew) { + // read both files if they are both new or both older than the last read + QStringList loadOrder = readLoadOrderList(pluginList, loadOrderPath); + pluginList->setLoadOrder(loadOrder); + readPluginList(pluginList); + } else { + // If the plugins is new but not loadorder, we must reparse the load order from the + // plugin files + QStringList loadOrder = readPluginList(pluginList); + pluginList->setLoadOrder(loadOrder); + } + + m_LastRead = QDateTime::currentDateTime(); +} + +void MorrowindGamePlugins::writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) +{ + return writeList(pluginList, filePath, false); +} + +void MorrowindGamePlugins::writeList(const IPluginList* pluginList, + const QString& filePath, bool loadOrder) +{ + QStringEncoder encoder = loadOrder + ? QStringEncoder(QStringConverter::Encoding::Utf8) + : QStringEncoder(QStringConverter::Encoding::System); + + QSettings settings(filePath, QSettings::IniFormat); + settings.remove("Game Files"); + + bool invalidFileNames = false; + int writtenCount = 0; + + QStringList plugins = pluginList->pluginNames(); + std::sort(plugins.begin(), plugins.end(), + [pluginList](const QString& lhs, const QString& rhs) { + return pluginList->priority(lhs) < pluginList->priority(rhs); + }); + QString key = "GameFile"; + for (const QString& pluginName : plugins) { + if (loadOrder || (pluginList->state(pluginName) == IPluginList::STATE_ACTIVE)) { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + if (!MOBase::WriteRegistryValue( + "Game Files", key + QString::number(writtenCount), pluginName, + filePath)) { + qWarning("failed to set game files in \"%s\"", qUtf8Printable(filePath)); + } + } + ++writtenCount; + } + } + + if (invalidFileNames) { + reportError(QObject::tr("Some of your plugins have invalid names! These " + "plugins can not be loaded by the game. Please see " + "mo_interface.log for a list of affected plugins " + "and rename them.")); + } + + if (writtenCount == 0) { + qWarning("plugin list would be empty, this is almost certainly wrong. Not " + "saving."); + } +} + +QStringList MorrowindGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + QStringList primary = organizer()->managedGame()->primaryPlugins(); + for (const QString& pluginName : primary) { + if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + } + } + QStringList plugins = pluginList->pluginNames(); + // Do not sort the primary plugins. Their load order should be locked as defined in + // "primaryPlugins". + const QStringList pluginsClone(plugins); + for (QString plugin : pluginsClone) { + if (primary.contains(plugin, Qt::CaseInsensitive)) + plugins.removeAll(plugin); + } + + // Always use filetime loadorder to get the actual load order + std::sort(plugins.begin(), plugins.end(), + [&](const QString& lhs, const QString& rhs) { + MOBase::IModInterface* lhm = + organizer()->modList()->getMod(pluginList->origin(lhs)); + MOBase::IModInterface* rhm = + organizer()->modList()->getMod(pluginList->origin(rhs)); + QDir lhd = organizer()->managedGame()->dataDirectory(); + QDir rhd = organizer()->managedGame()->dataDirectory(); + if (lhm != nullptr) + lhd = lhm->absolutePath(); + if (rhm != nullptr) + rhd = rhm->absolutePath(); + QString lhp = lhd.absoluteFilePath(lhs); + QString rhp = rhd.absoluteFilePath(rhs); + return QFileInfo(lhp).lastModified() < QFileInfo(rhp).lastModified(); + }); + + QString filePath = organizer()->profile()->absolutePath() + "/Morrowind.ini"; + if (!organizer()->profile()->localSettingsEnabled()) { + filePath = + organizer()->managedGame()->gameDirectory().absolutePath() + "/Morrowind.ini"; + } + QStringList activePlugins; + QStringList inactivePlugins; + QSettings settings(filePath, QSettings::IniFormat); + QString key = "GameFile"; + int i = 0; + while (true) { + QString pluginName = + settings.value("Game Files/" + key + QString::number(i), "").toString().trimmed(); + if (pluginName.isEmpty()) { + break; + } + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + activePlugins.push_back(pluginName); + i++; + } + + // we removed each plugin found in the file, so what's left are inactive mods + for (const QString& pluginName : plugins) + if (!activePlugins.contains(pluginName)) + inactivePlugins.push_back(pluginName); + + for (const QString& pluginName : inactivePlugins) + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + + return primary + plugins; +} diff --git a/libs/game_bethesda/src/games/morrowind/morrowindgameplugins.h b/libs/game_bethesda/src/games/morrowind/morrowindgameplugins.h new file mode 100644 index 0000000..37bc219 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowindgameplugins.h @@ -0,0 +1,28 @@ +#ifndef MORROWINDGAMEPLUGINS_H +#define MORROWINDGAMEPLUGINS_H + +#include + +class MorrowindGamePlugins : public GamebryoGamePlugins +{ + +public: + MorrowindGamePlugins(MOBase::IOrganizer* organizer); + + virtual void writePluginLists(const MOBase::IPluginList* pluginList) override; + virtual void readPluginLists(MOBase::IPluginList* pluginList) override; + +protected: + virtual void writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) override; + virtual QStringList readPluginList(MOBase::IPluginList* pluginList) override; + +private: + virtual void writeList(const MOBase::IPluginList* pluginList, const QString& filePath, + bool loadOrder); + +private: + QDateTime m_LastRead; +}; + +#endif // MORROWINDGAMEPLUGINS_H diff --git a/libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.cpp b/libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.cpp new file mode 100644 index 0000000..7499a58 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.cpp @@ -0,0 +1,56 @@ +/* +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "morrowindlocalsavegames.h" +#include +#include +#include +#include + +MorrowindLocalSavegames::MorrowindLocalSavegames(const MOBase::IPluginGame* game) + : m_GamePlugin(game) +{} + +bool MorrowindLocalSavegames::prepareProfile(MOBase::IProfile* profile) +{ + bool dirty = false; + + if (profile->localSavesEnabled()) { + if (m_GamePlugin->gameDirectory().exists("Saves")) { + if (!m_GamePlugin->gameDirectory().rename("Saves", "_Saves")) { + qCritical("Unable to enable Morrowind local save games!"); + } + dirty = true; + } + } else { + if (m_GamePlugin->gameDirectory().exists("_Saves")) { + if (!m_GamePlugin->gameDirectory().rename("_Saves", "Saves")) { + qCritical("Unable to disable Morrowind local save games!"); + } + dirty = true; + } + } + + return dirty; +} + +MappingType MorrowindLocalSavegames::mappings(const QDir& profileSaveDir) const +{ + return {{profileSaveDir.absolutePath(), + m_GamePlugin->gameDirectory().absoluteFilePath("Saves"), true, true}}; +} diff --git a/libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.h b/libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.h new file mode 100644 index 0000000..e391e12 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.h @@ -0,0 +1,41 @@ +/* +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef MORROWINDLOCALSAVEGAMES_H +#define MORROWINDLOCALSAVEGAMES_H + +#include + +#include "iplugingame.h" +#include +#include + +class MorrowindLocalSavegames : public MOBase::LocalSavegames +{ + +public: + MorrowindLocalSavegames(const MOBase::IPluginGame* game); + + virtual MappingType mappings(const QDir& profileSaveDir) const override; + virtual bool prepareProfile(MOBase::IProfile* profile) override; + +private: + const MOBase::IPluginGame* m_GamePlugin; +}; + +#endif // MORROWINDLOCALSAVEGAMES_H diff --git a/libs/game_bethesda/src/games/morrowind/morrowindmoddatachecker.h b/libs/game_bethesda/src/games/morrowind/morrowindmoddatachecker.h new file mode 100644 index 0000000..bd158e0 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowindmoddatachecker.h @@ -0,0 +1,26 @@ +#ifndef MORROWIND_MODATACHECKER_H +#define MORROWIND_MODATACHECKER_H + +#include + +class MorrowindModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{"fonts", "meshes", "music", "shaders", "sound", + "textures", "video", "mwse", "distantland", "mits", + "icons", "bookart", "splash", "animations"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "bsa", "modgroups"}; + return result; + } +}; + +#endif // MORROWIND_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/morrowind/morrowindmoddatacontent.h b/libs/game_bethesda/src/games/morrowind/morrowindmoddatacontent.h new file mode 100644 index 0000000..b8c9af5 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowindmoddatacontent.h @@ -0,0 +1,24 @@ +#ifndef MORROWIND_MODDATACONTENT_H +#define MORROWIND_MODDATACONTENT_H + +#include +#include + +class MorrowindModDataContent : public GamebryoModDataContent +{ +public: + /** + * + */ + MorrowindModDataContent(MOBase::IGameFeatures const* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + // Just need to disable some contents: + m_Enabled[CONTENT_MCM] = false; + m_Enabled[CONTENT_SKYPROC] = false; + m_Enabled[CONTENT_INTERFACE] = false; + m_Enabled[CONTENT_SCRIPT] = false; + } +}; + +#endif // MORROWIND_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/morrowind/morrowindsavegame.cpp b/libs/game_bethesda/src/games/morrowind/morrowindsavegame.cpp new file mode 100644 index 0000000..9588563 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowindsavegame.cpp @@ -0,0 +1,183 @@ +#include "morrowindsavegame.h" + +#include +#include +#include + +MorrowindSaveGame::MorrowindSaveGame(QString const& fileName, GameMorrowind const* game) + : GamebryoSaveGame(fileName, game) +{ + std::filesystem::path realFile(fileName.toStdWString()); + QString realFileName = QString::fromStdWString(realFile.filename().wstring()); + m_SaveNumber = realFileName.mid(4, 5).remove(QRegularExpression("0+$")).toInt(); + + FileWrapper file(fileName, "TES3"); + QStringList dummyPlugins; + fetchInformationFields(file, m_SaveName, dummyPlugins, m_PCCurrentHealth, + m_PCCMaxHealth, m_PCLocation, m_GameDays, m_PCName); +} + +QString MorrowindSaveGame::getName() const +{ + return QString("%1, #%2, %3").arg(m_PCName).arg(m_SaveNumber).arg(m_PCLocation); +} + +unsigned short MorrowindSaveGame::getPCLevel() const +{ + return dynamic_cast(m_DataFields.value().get())->PCLevel; +} + +// Fetch easy-to-access information. +void MorrowindSaveGame::fetchInformationFields(FileWrapper& file, QString& saveName, + QStringList& plugins, + float& playerCurrentHealth, + float& playerMaxHealth, + QString& playerLocation, float& gameDays, + QString& playerName) const +{ + file.skip(3); // data size + file.skip(4); // HEDR tag + file.skip(); // header size + file.skip(); // header version + file.skip(); // following data chunk size? seems to be 9 groupings of 32 + // bytes + file.skip(32); // Author empty for save files + + // The defined save name. This is technically the description, but is likely only + // 31+\0 chars max. + { + std::vector saveNameBuffer(256); // 31 char save name with a null terminator + file.read(saveNameBuffer.data(), 256); + saveName = QString::fromLatin1(saveNameBuffer.data(), -1).trimmed(); + } + + file.skip(); // NumRecords (for the entire save) + std::vector buffer(255); + file.read(buffer.data(), 4); + // Parse the MAST/DATA records + while (QString::fromLatin1(buffer.data(), 4) == "MAST") { + uint32_t len; + file.read(len); // Length of master name + file.read(buffer.data(), len); // Name of master + QString name = QString::fromLatin1(buffer.data(), len - 1); + file.skip(4); // DATA record + + // Typically size 8 - contains length of master data for version checking + file.read(len); // Length + file.skip(len); + + file.read(buffer.data(), 4); // Get next record type + plugins.push_back(name); + } + + // Start of GMDT + file.skip(); // size of record + + file.read(playerCurrentHealth); + file.read(playerMaxHealth); + + file.skip(); // current stam? + file.skip(); // max stam? + // file.skip(2); // unknown values + + std::fill(buffer.begin(), buffer.end(), '\0'); + file.read(buffer.data(), 64); + playerLocation = QString::fromLatin1(buffer.data(), -1).trimmed(); + + file.read(gameDays); + + std::fill(buffer.begin(), buffer.end(), '\0'); + file.read(buffer.data(), 32); + playerName = QString::fromLatin1(buffer.data(), -1).trimmed(); + + // End of GMDT +} + +std::unique_ptr MorrowindSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TES3"); + std::vector buffer(255); + + std::unique_ptr fields = std::make_unique(); + + { + QString dummy; + float dummyF; + fetchInformationFields(file, dummy, fields->Plugins, dummyF, dummyF, dummy, dummyF, + dummy); + } + + file.skip(28); // Skip the SCRD + // I believe this tells the engine what color each pixel represents and the bitness of + // the image + + // Start of screenshot + file.skip(4); // SCRS + file.skip(); // Size of screenshot always 65536 (128x128x4) RGBA8888 + + QImage image = readImageBGRA(file, 128, 128, 0, 1); + fields->Screenshot = image.scaled(252, 192); + + // definitively have to use another method to access the player level + // it is stored in the fifth byte of the NPDT subrecord of the first NPC_ record + + // Globals, Scripts, Regions + // file.skip(); + std::vector buff(4); + file.read(buff.data(), 4); + while (QString::fromLatin1(buff.data(), 4) != "NPC_") { + uint32_t len; + file.read(len); + file.skip(8 + len); + file.read(buff.data(), 4); + } + while (QString::fromLatin1(buff.data(), 4) == "NPC_") { + uint32_t size; + file.read(size); + file.skip(3); + uint32_t len; + file.read(len); + file.read(buffer.data(), len); + if (QString::fromLatin1(buffer.data(), len - 1) == "player") { + file.read(buff.data(), 4); + while (QString::fromLatin1(buff.data(), 4) != "NPDT") { + uint32_t len; + file.read(len); + file.skip(len); + file.read(buff.data(), 4); + } + file.skip(); + file.read(fields->PCLevel); + } else { + file.skip(size - len - 8); + } + } + + return fields; +} + +QImage MorrowindSaveGame::readImageBGRA(GamebryoSaveGame::FileWrapper& file, + unsigned long width, unsigned long height, + int scale = 0, bool alpha = false) const +{ + QImage image(width, height, QImage::Format_RGBA8888); + for (unsigned long h = 0; h < width; h++) { + for (unsigned long w = 0; w < width; w++) { + uint8_t blue; + file.read(blue); + uint8_t green; + file.read(green); + uint8_t red; + file.read(red); + uint8_t alpha; + file.read(alpha); + alpha = 255 - alpha; + QColor color(red, green, blue, alpha); + image.setPixel(w, h, color.rgba()); + } + } + if (scale != 0) + return image.copy().scaledToWidth(scale); + else + return image.copy(); +} diff --git a/libs/game_bethesda/src/games/morrowind/morrowindsavegame.h b/libs/game_bethesda/src/games/morrowind/morrowindsavegame.h new file mode 100644 index 0000000..b4ea960 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowindsavegame.h @@ -0,0 +1,57 @@ +#ifndef MORROWINDSAVEGAME_H +#define MORROWINDSAVEGAME_H + +#include "gamebryosavegame.h" +#include "gamemorrowind.h" + +namespace MOBase +{ +class IPluginGame; +} + +class MorrowindSaveGame : public GamebryoSaveGame +{ +public: + MorrowindSaveGame(QString const& fileName, GameMorrowind const* game); + +public: // ISaveGame interface + // We need to override getName() because we do not read the level at + // the beginning. + virtual QString getName() const override; + + // The PC level is not pre-fetch for morrowind. + unsigned short getPCLevel() const override; + +public: + // Simple getters + QString getSaveName() const { return m_SaveName; } + float getPCCurrentHealth() const { return m_PCCurrentHealth; } + float getPCMaxHealth() const { return m_PCCMaxHealth; } + float getGameDays() const { return m_GameDays; } + +protected: + QString m_SaveName; + float m_PCCurrentHealth; + float m_PCCMaxHealth; + float m_GameDays; + +protected: + QImage readImageBGRA(GamebryoSaveGame::FileWrapper& file, unsigned long width, + unsigned long height, int scale, bool alpha) const; + + // We need to add the PC level here. + struct MorrowindDataFields : public DataFields + { + unsigned short PCLevel = 0; + }; + + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& file, QString& saveName, + QStringList& plugins, float& playerCurrentHealth, + float& playerMaxHealth, QString& playerLocation, + float& gameDays, QString& playerName) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // MORROWINDSAVEGAME_H diff --git a/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.cpp b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.cpp new file mode 100644 index 0000000..746ea50 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.cpp @@ -0,0 +1,18 @@ +#include "morrowindsavegameinfo.h" +#include "gamegamebryo.h" +#include "morrowindsavegame.h" +#include "morrowindsavegameinfowidget.h" + +MorrowindSaveGameInfo::MorrowindSaveGameInfo(GameGamebryo const* game) + : GamebryoSaveGameInfo(game) +{ + m_Game = dynamic_cast(game); +} + +MorrowindSaveGameInfo::~MorrowindSaveGameInfo() {} + +MOBase::ISaveGameInfoWidget* +MorrowindSaveGameInfo::getSaveGameWidget(QWidget* parent) const +{ + return new MorrowindSaveGameInfoWidget(this, parent); +} diff --git a/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.h b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.h new file mode 100644 index 0000000..ff4bc55 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.h @@ -0,0 +1,22 @@ +#ifndef MORROWINDSAVEGAMEINFO_H +#define MORROWINDSAVEGAMEINFO_H + +#include "gamebryosavegameinfo.h" +#include "gamemorrowind.h" + +class GameGamebryo; + +class MorrowindSaveGameInfo : public GamebryoSaveGameInfo +{ +public: + MorrowindSaveGameInfo(GameGamebryo const* game); + ~MorrowindSaveGameInfo(); + + virtual MOBase::ISaveGameInfoWidget* getSaveGameWidget(QWidget*) const override; + +protected: + friend class MorrowindSaveGameInfoWidget; + GameMorrowind const* m_Game; +}; + +#endif // MORROWINDSAVEGAMEINFO_H diff --git a/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.cpp b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.cpp new file mode 100644 index 0000000..9e098fb --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.cpp @@ -0,0 +1,123 @@ +#include "morrowindsavegameinfowidget.h" +#include "ui_morrowindsavegameinfowidget.h" + +#include "gamemorrowind.h" +#include "imoinfo.h" +#include "ipluginlist.h" +#include "morrowindsavegame.h" +#include "morrowindsavegameinfo.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +MorrowindSaveGameInfoWidget::MorrowindSaveGameInfoWidget( + MorrowindSaveGameInfo const* info, QWidget* parent) + : MOBase::ISaveGameInfoWidget(parent), ui(new Ui::MorrowindSaveGameInfoWidget), + m_Info(info) +{ + ui->setupUi(this); + this->setWindowFlags(Qt::ToolTip | Qt::BypassGraphicsProxyWidget); + setWindowOpacity(style()->styleHint(QStyle::SH_ToolTipLabel_Opacity, 0, this) / + qreal(255.0)); + ui->gameFrame->setStyleSheet("background-color: transparent;"); + + QVBoxLayout* gameLayout = new QVBoxLayout(); + gameLayout->setContentsMargins(0, 0, 0, 0); + gameLayout->setSpacing(2); + ui->gameFrame->setLayout(gameLayout); +} + +MorrowindSaveGameInfoWidget::~MorrowindSaveGameInfoWidget() +{ + delete ui; +} + +void MorrowindSaveGameInfoWidget::setSave(MOBase::ISaveGame const& save) +{ + auto const& morrowindSave = dynamic_cast(save); + + ui->saveNameLabel->setText(QString("%1 (Day %2)") + .arg(morrowindSave.getSaveName()) + .arg(morrowindSave.getGameDays())); + ui->saveNumLabel->setText(QString("%1").arg(morrowindSave.getSaveNumber())); + ui->healthLabel->setText(QString("%1 / %2") + .arg(round(morrowindSave.getPCCurrentHealth())) + .arg(morrowindSave.getPCMaxHealth())); + ui->characterLabel->setText(morrowindSave.getPCName()); + ui->locationLabel->setText(morrowindSave.getPCLocation()); + ui->levelLabel->setText(QString("%1").arg(morrowindSave.getPCLevel())); + + // This somewhat contorted code is because on my system at least, the + // old way of doing this appears to give short date and long time. + QDateTime t = morrowindSave.getCreationTime(); + ui->dateLabel->setText( + QLocale::system().toString(t.date(), QLocale::FormatType::ShortFormat) + " " + + QLocale::system().toString(t.time(), QLocale::FormatType::ShortFormat)); + ui->screenshotLabel->setPixmap(QPixmap::fromImage(morrowindSave.getScreenshot())); + if (ui->gameFrame->layout() != nullptr) { + QLayoutItem* item = nullptr; + while ((item = ui->gameFrame->layout()->takeAt(0)) != nullptr) { + delete item->widget(); + delete item; + } + ui->gameFrame->layout()->setSizeConstraint(QLayout::SetFixedSize); + } + + // Resize box to new content + this->resize(0, 0); + + QLayout* layout = ui->gameFrame->layout(); + QLabel* header = new QLabel(tr("Missing ESPs")); + QFont headerFont = header->font(); + QFont contentFont = headerFont; + headerFont.setItalic(true); + contentFont.setBold(true); + contentFont.setPointSize(7); + header->setFont(headerFont); + layout->addWidget(header); + int count = 0; + MOBase::IPluginList* pluginList = m_Info->m_Game->m_Organizer->pluginList(); + for (QString const& pluginName : morrowindSave.getPlugins()) { + if (pluginList->state(pluginName) == MOBase::IPluginList::STATE_ACTIVE) { + continue; + } + + ++count; + + if (count > 7) { + break; + } + + QLabel* pluginLabel = new QLabel(pluginName); + pluginLabel->setIndent(10); + pluginLabel->setFont(contentFont); + layout->addWidget(pluginLabel); + } + if (count > 7) { + QLabel* dotDotLabel = new QLabel("..."); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + if (count == 0) { + QLabel* dotDotLabel = new QLabel(tr("None")); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } +} diff --git a/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.h b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.h new file mode 100644 index 0000000..af27bc3 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.h @@ -0,0 +1,31 @@ +#ifndef MORROWINDSAVEGAMEINFOWIDGET_H +#define MORROWINDSAVEGAMEINFOWIDGET_H + +#include "isavegameinfowidget.h" +#include "morrowindsavegameinfo.h" + +#include + +class GamebryoGame; + +namespace Ui +{ +class MorrowindSaveGameInfoWidget; +} + +class MorrowindSaveGameInfoWidget : public MOBase::ISaveGameInfoWidget +{ + Q_OBJECT + +public: + MorrowindSaveGameInfoWidget(MorrowindSaveGameInfo const* info, QWidget* parent); + ~MorrowindSaveGameInfoWidget(); + + virtual void setSave(MOBase::ISaveGame const&) override; + +private: + Ui::MorrowindSaveGameInfoWidget* ui; + MorrowindSaveGameInfo const* m_Info; +}; + +#endif // MORROWINDSAVEGAMEINFOWIDGET_H diff --git a/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.ui b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.ui new file mode 100644 index 0000000..9ba6ba1 --- /dev/null +++ b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.ui @@ -0,0 +1,248 @@ + + + MorrowindSaveGameInfoWidget + + + + 0 + 0 + 400 + 300 + + + + + 0 + 0 + + + + + + + + + + + + + + 10 + 75 + true + + + + + + + + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + + true + + + + Save # + + + + + + + + 75 + true + + + + + + + + + + + + true + + + + Character + + + + + + + + 75 + true + + + + + + + + + + + + true + + + + Level + + + + + + + + 75 + true + + + + + + + + + + + + true + + + + Health + + + + + + + + 75 + true + + + + + + + + + + + + true + + + + Location + + + + + + + + 75 + true + + + + + + + + + + + + true + + + + Date + + + + + + + + 75 + true + + + + + + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + false + + + + + + Qt::AlignCenter + + + + + + + + diff --git a/libs/game_bethesda/src/games/morrowind/splash.png b/libs/game_bethesda/src/games/morrowind/splash.png new file mode 100644 index 0000000..7914180 Binary files /dev/null and b/libs/game_bethesda/src/games/morrowind/splash.png differ diff --git a/libs/game_bethesda/src/games/nehrim/CMakeLists.txt b/libs/game_bethesda/src/games/nehrim/CMakeLists.txt new file mode 100644 index 0000000..a1c1c1f --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_nehrim SHARED + gamenehrim.cpp + gamenehrim.h + nehrimbsainvalidation.cpp + nehrimbsainvalidation.h + nehrimdataarchives.cpp + nehrimdataarchives.h + nehrimmoddatachecker.cpp + nehrimmoddatachecker.h + nehrimmoddatacontent.h + nehrimsavegame.cpp + nehrimsavegame.h + nehrimscriptextender.cpp + nehrimscriptextender.h +) +mo2_configure_plugin(game_nehrim NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_nehrim PRIVATE game_gamebryo) +mo2_install_plugin(game_nehrim) diff --git a/libs/game_bethesda/src/games/nehrim/game_nehrim_en.ts b/libs/game_bethesda/src/games/nehrim/game_nehrim_en.ts new file mode 100644 index 0000000..b5c79e4 --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/game_nehrim_en.ts @@ -0,0 +1,17 @@ + + + + + GameNehrim + + + Nehrim Support Plugin + + + + + Adds support for the game Nehrim + + + + diff --git a/libs/game_bethesda/src/games/nehrim/gamenehrim.cpp b/libs/game_bethesda/src/games/nehrim/gamenehrim.cpp new file mode 100644 index 0000000..2e3c8c5 --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/gamenehrim.cpp @@ -0,0 +1,205 @@ +#include "gamenehrim.h" + +#include "nehrimbsainvalidation.h" +#include "nehrimdataarchives.h" +#include "nehrimmoddatachecker.h" +#include "nehrimmoddatacontent.h" +#include "nehrimsavegame.h" +#include "nehrimscriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" +#include +#include +#include +#include + +#include +#include +#include + +#include + +using namespace MOBase; + +GameNehrim::GameNehrim() {} + +bool GameNehrim::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "oblivion.ini")); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + return true; +} + +QString GameNehrim::gameName() const +{ + return "Nehrim"; +} + +QList GameNehrim::executables() const +{ + return QList() + << ExecutableInfo("Nehrim", findInGameFolder("Oblivion.exe")) + << ExecutableInfo("Nehrim Launcher", findInGameFolder("NehrimLauncher.exe")) + << ExecutableInfo("Oblivion Mod Manager", + findInGameFolder("OblivionModManager.exe")) + << ExecutableInfo("BOSS", findInGameFolder("BOSS/BOSS.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Nehrim\"") + << ExecutableInfo("Construction Set", + findInGameFolder("TESConstructionSet.exe")); +} + +QList GameNehrim::executableForcedLoads() const +{ + // TODO Search game directory for OBSE DLLs + return QList() + << ExecutableForcedLoadSetting("Oblivion.exe", "obse_1_2_416.dll") + .withForced() + .withEnabled() + << ExecutableForcedLoadSetting("TESConstructionSet.exe", "obse_editor_1_2.dll") + .withForced() + .withEnabled(); +} + +QString GameNehrim::name() const +{ + return "Nehrim Support Plugin"; +} + +QString GameNehrim::localizedName() const +{ + return tr("Nehrim Support Plugin"); +} + +QString GameNehrim::author() const +{ + return "Tannin & MO2 Team"; +} + +QString GameNehrim::description() const +{ + return tr("Adds support for the game Nehrim"); +} + +MOBase::VersionInfo GameNehrim::version() const +{ + return VersionInfo(1, 1, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameNehrim::settings() const +{ + return QList(); +} + +void GameNehrim::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Oblivion", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/oblivion.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "oblivion_default.ini", + "oblivion.ini"); + } else { + copyToProfile(myGamesPath(), path, "oblivion.ini"); + } + + copyToProfile(myGamesPath(), path, "oblivionprefs.ini"); + } +} + +QString GameNehrim::savegameExtension() const +{ + return "ess"; +} + +QString GameNehrim::savegameSEExtension() const +{ + return "obse"; +} + +std::shared_ptr GameNehrim::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameNehrim::steamAPPId() const +{ + return "22330"; +} + +QStringList GameNehrim::primaryPlugins() const +{ + return {"Nehrim.esm", "Translation.esp"}; +} + +QString GameNehrim::gameShortName() const +{ + return "Nehrim"; +} + +QString GameNehrim::gameNexusName() const +{ + return "Nehrim"; +} + +QStringList GameNehrim::iniFiles() const +{ + return {"oblivion.ini", "oblivionprefs.ini"}; +} + +QStringList GameNehrim::DLCPlugins() const +{ + return {}; +} + +int GameNehrim::nexusModOrganizerID() const +{ + return -1; +} + +int GameNehrim::nexusGameID() const +{ + return 3312; +} + +QStringList GameNehrim::primarySources() const +{ + return {"Oblivion"}; +} + +QStringList GameNehrim::validShortNames() const +{ + return {"Oblivion"}; +} + +QString GameNehrim::identifyGamePath() const +{ +#ifdef _WIN32 + QString path = "Software\\Bethesda Softworks\\Oblivion"; + return findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(), + L"Installed Path"); +#else + return GameGamebryo::identifyGamePath(); +#endif +} + +QString GameNehrim::binaryName() const +{ + return "NehrimLauncher.exe"; +} diff --git a/libs/game_bethesda/src/games/nehrim/gamenehrim.h b/libs/game_bethesda/src/games/nehrim/gamenehrim.h new file mode 100644 index 0000000..a8af24c --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/gamenehrim.h @@ -0,0 +1,56 @@ +#ifndef GAMENEHRIM_H +#define GAMENEHRIM_H + +#include "gamegamebryo.h" + +#include +#include + +class GameNehrim : public GameGamebryo +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.tannin.GameNehrim" FILE "gamenehrim.json") + +public: + GameNehrim(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QString gameShortName() const override; + virtual QString gameNexusName() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + virtual QStringList primarySources() const override; + virtual QStringList validShortNames() const override; + + // Weird stuff happens in these functions due to Nehrim + // technically being in the Oblivion folder + virtual QString identifyGamePath() const override; + virtual QString binaryName() const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + +protected: + std::shared_ptr makeSaveGame(QString filePath) const override; + QString savegameExtension() const override; + QString savegameSEExtension() const override; +}; + +#endif // GAMENEHRIM_H diff --git a/libs/game_bethesda/src/games/nehrim/gamenehrim.json b/libs/game_bethesda/src/games/nehrim/gamenehrim.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/gamenehrim.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.cpp b/libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.cpp new file mode 100644 index 0000000..ac89d54 --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "nehrimbsainvalidation.h" + +NehrimBSAInvalidation::NehrimBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "oblivion.ini", game) +{} + +QString NehrimBSAInvalidation::invalidationBSAName() const +{ + return "Nehrim - Invalidation.bsa"; +} + +unsigned long NehrimBSAInvalidation::bsaVersion() const +{ + return 0x67; +} diff --git a/libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.h b/libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.h new file mode 100644 index 0000000..3a2e2b3 --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.h @@ -0,0 +1,20 @@ +#ifndef NEHRIMBSAINVALIDATION_H +#define NEHRIMBSAINVALIDATION_H + +#include "gamebryobsainvalidation.h" +#include "nehrimdataarchives.h" + +#include + +class NehrimBSAInvalidation : public GamebryoBSAInvalidation +{ +public: + NehrimBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; +}; + +#endif // NEHRIMBSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/nehrim/nehrimdataarchives.cpp b/libs/game_bethesda/src/games/nehrim/nehrimdataarchives.cpp new file mode 100644 index 0000000..8fe5b06 --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/nehrimdataarchives.cpp @@ -0,0 +1,31 @@ +#include "nehrimdataarchives.h" +#include + +QStringList NehrimDataArchives::vanillaArchives() const +{ + return {"N - Meshes.bsa", "N - Textures1.bsa", "N - Textures2.bsa", "N - Misc.bsa", + "N - Sounds.bsa", "L - Voices.bsa", "L - Misc.bsa"}; +} + +QStringList NehrimDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("oblivion.ini") + : localGameDirectory().absoluteFilePath("oblivion.ini"); + result.append(getArchivesFromKey(iniFile, "SArchiveList")); + + return result; +} + +void NehrimDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("oblivion.ini") + : localGameDirectory().absoluteFilePath("oblivion.ini"); + setArchivesToKey(iniFile, "SArchiveList", list); +} diff --git a/libs/game_bethesda/src/games/nehrim/nehrimdataarchives.h b/libs/game_bethesda/src/games/nehrim/nehrimdataarchives.h new file mode 100644 index 0000000..2b6425c --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/nehrimdataarchives.h @@ -0,0 +1,23 @@ +#ifndef NEHRIMDATAARCHIVES_H +#define NEHRIMDATAARCHIVES_H + +#include +#include +#include +#include +#include + +class NehrimDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // NEHRIMDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/nehrim/nehrimmoddatachecker.cpp b/libs/game_bethesda/src/games/nehrim/nehrimmoddatachecker.cpp new file mode 100644 index 0000000..3ec4eb0 --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/nehrimmoddatachecker.cpp @@ -0,0 +1,30 @@ +#include "nehrimmoddatachecker.h" + +MOBase::ModDataChecker::CheckReturn NehrimModDataChecker::dataLooksValid( + std::shared_ptr fileTree) const +{ + // Check with Gamebryo stuff: + auto check = GamebryoModDataChecker::dataLooksValid(fileTree); + if (check == CheckReturn::VALID) { + return check; + } + + // Check for OBSE_ files: + for (auto const& entry : *fileTree) { + if (entry->isDir() || !entry->name().startsWith("OBSE", Qt::CaseInsensitive)) { + return CheckReturn::INVALID; + } + } + + return CheckReturn::FIXABLE; +} + +std::shared_ptr +NehrimModDataChecker::fix(std::shared_ptr fileTree) const +{ + // If we arrive here, it means all files starts with OBSE. + auto data = fileTree->createOrphanTree(); + auto obse = data->addDirectory("OBSE/Plugins"); + obse->merge(fileTree); + return data; +} diff --git a/libs/game_bethesda/src/games/nehrim/nehrimmoddatachecker.h b/libs/game_bethesda/src/games/nehrim/nehrimmoddatachecker.h new file mode 100644 index 0000000..c8717dd --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/nehrimmoddatachecker.h @@ -0,0 +1,35 @@ +#ifndef NEHRIM_MODATACHECKER_H +#define NEHRIM_MODATACHECKER_H + +#include + +class NehrimModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + + CheckReturn + dataLooksValid(std::shared_ptr fileTree) const override; + std::shared_ptr + fix(std::shared_ptr fileTree) const override; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{"fonts", "interface", "menus", + "meshes", "music", "scripts", + "shaders", "sound", "strings", + "textures", "trees", "video", + "facegen", "obse", "distantlod", + "asi", "distantland", "mits", + "dllplugins", "CalienteTools", "NetScriptFramework"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "bsa", "modgroups", "ini"}; + return result; + } +}; + +#endif // NEHRIM_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/nehrim/nehrimmoddatacontent.h b/libs/game_bethesda/src/games/nehrim/nehrimmoddatacontent.h new file mode 100644 index 0000000..c640377 --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/nehrimmoddatacontent.h @@ -0,0 +1,22 @@ +#ifndef NEHRIM_MODDATACONTENT_H +#define NEHRIM_MODDATACONTENT_H + +#include +#include + +class NehrimModDataContent : public GamebryoModDataContent +{ +public: + /** + * + */ + NehrimModDataContent(const MOBase::IGameFeatures* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + // Just need to disable some contents: + m_Enabled[CONTENT_MCM] = false; + m_Enabled[CONTENT_SKYPROC] = false; + } +}; + +#endif // NEHRIM_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/nehrim/nehrimsavegame.cpp b/libs/game_bethesda/src/games/nehrim/nehrimsavegame.cpp new file mode 100644 index 0000000..4a6f912 --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/nehrimsavegame.cpp @@ -0,0 +1,72 @@ +#include "nehrimsavegame.h" + +NehrimSaveGame::NehrimSaveGame(QString const& fileName, GameNehrim const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "TES4SAVEGAME"); + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING); + + _SYSTEMTIME creationTime; + fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, + creationTime); + setCreationTime(creationTime); +} + +void NehrimSaveGame::fetchInformationFields(FileWrapper& file, + uint32_t& saveNumber, + QString& playerName, + unsigned short& playerLevel, + QString& playerLocation, + _SYSTEMTIME& creationTime) const +{ + file.skip(); // Major version + file.skip(); // Minor version + + file.skip<_SYSTEMTIME>(); // exe last modified (!) + + file.skip(); // Header version + file.skip(); // Header size + + file.read(saveNumber); + + file.read(playerName); + file.read(playerLevel); + file.read(playerLocation); + + file.skip(); // game days + file.skip(); // game ticks + + // there is a save time stored here. So use it rather than the file time, which + // could have been copied. + // Note: This says it uses getlocaltime api to obtain it which is u/s - if so + // we should ignore this. + file.read(creationTime); +} + +std::unique_ptr NehrimSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TES4SAVEGAME"); + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING); + + std::unique_ptr fields = std::make_unique(); + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + _SYSTEMTIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation, + dummyTime); + } + + // Note that screenshot size, width, height and data are apparently the same + // structure + file.skip(); // Screenshot size. + + fields->Screenshot = file.readImage(); + + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/nehrim/nehrimsavegame.h b/libs/game_bethesda/src/games/nehrim/nehrimsavegame.h new file mode 100644 index 0000000..cdbab22 --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/nehrimsavegame.h @@ -0,0 +1,21 @@ +#ifndef NEHRIMSAVEGAME_H +#define NEHRIMSAVEGAME_H + +#include "gamebryosavegame.h" +#include "gamenehrim.h" + +class NehrimSaveGame : public GamebryoSaveGame +{ +public: + NehrimSaveGame(QString const& fileName, GameNehrim const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& wrapper, uint32_t& saveNumber, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation, _SYSTEMTIME& creationTime) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // NEHRIMSAVEGAME_H diff --git a/libs/game_bethesda/src/games/nehrim/nehrimscriptextender.cpp b/libs/game_bethesda/src/games/nehrim/nehrimscriptextender.cpp new file mode 100644 index 0000000..c50c59e --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/nehrimscriptextender.cpp @@ -0,0 +1,20 @@ +#include "nehrimscriptextender.h" + +#include +#include + +NehrimScriptExtender::NehrimScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +NehrimScriptExtender::~NehrimScriptExtender() {} + +QString NehrimScriptExtender::BinaryName() const +{ + return "obse_loader.exe"; +} + +QString NehrimScriptExtender::PluginPath() const +{ + return "obse/plugins"; +} diff --git a/libs/game_bethesda/src/games/nehrim/nehrimscriptextender.h b/libs/game_bethesda/src/games/nehrim/nehrimscriptextender.h new file mode 100644 index 0000000..3bdac2a --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/nehrimscriptextender.h @@ -0,0 +1,18 @@ +#ifndef NEHRIMSCRIPTEXTENDER_H +#define NEHRIMSCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class NehrimScriptExtender : public GamebryoScriptExtender +{ +public: + NehrimScriptExtender(const GameGamebryo* game); + ~NehrimScriptExtender(); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // NEHRIMSCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/oblivion/CMakeLists.txt b/libs/game_bethesda/src/games/oblivion/CMakeLists.txt new file mode 100644 index 0000000..df5ff8c --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_oblivion SHARED + gameoblivion.cpp + gameoblivion.h + oblivionbsainvalidation.cpp + oblivionbsainvalidation.h + obliviondataarchives.cpp + obliviondataarchives.h + oblivionmoddatachecker.cpp + oblivionmoddatachecker.h + oblivionmoddatacontent.h + oblivionsavegame.cpp + oblivionsavegame.h + oblivionscriptextender.cpp + oblivionscriptextender.h +) +mo2_configure_plugin(game_oblivion NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_oblivion PRIVATE game_gamebryo) +mo2_install_plugin(game_oblivion) diff --git a/libs/game_bethesda/src/games/oblivion/game_oblivion_en.ts b/libs/game_bethesda/src/games/oblivion/game_oblivion_en.ts new file mode 100644 index 0000000..80b1dbb --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/game_oblivion_en.ts @@ -0,0 +1,17 @@ + + + + + GameOblivion + + + Oblivion Support Plugin + + + + + Adds support for the game Oblivion + + + + diff --git a/libs/game_bethesda/src/games/oblivion/gameoblivion.cpp b/libs/game_bethesda/src/games/oblivion/gameoblivion.cpp new file mode 100644 index 0000000..9f7a174 --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/gameoblivion.cpp @@ -0,0 +1,213 @@ +#include "gameoblivion.h" + +#include "oblivionbsainvalidation.h" +#include "obliviondataarchives.h" +#include "oblivionmoddatachecker.h" +#include "oblivionmoddatacontent.h" +#include "oblivionsavegame.h" +#include "oblivionscriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" +#include "steamutility.h" +#include +#include +#include +#include + +#include +#include +#include + +#include + +using namespace MOBase; + +GameOblivion::GameOblivion() {} + +bool GameOblivion::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "oblivion.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + return true; +} + +QString GameOblivion::gameName() const +{ + return "Oblivion"; +} + +QList GameOblivion::executables() const +{ + return QList() + << ExecutableInfo("Oblivion", findInGameFolder(binaryName())) + << ExecutableInfo("Oblivion Launcher", findInGameFolder(getLauncherName())) + << ExecutableInfo("Oblivion Mod Manager", + findInGameFolder("OblivionModManager.exe")) + << ExecutableInfo("BOSS", findInGameFolder("BOSS/BOSS.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Oblivion\"") + << ExecutableInfo("Construction Set", + findInGameFolder("TESConstructionSet.exe")); +} + +QList GameOblivion::executableForcedLoads() const +{ + return QList() + << ExecutableForcedLoadSetting("Oblivion.exe", "obse_1_2_416.dll") + .withForced() + .withEnabled() + << ExecutableForcedLoadSetting("TESConstructionSet.exe", "obse_editor_1_2.dll") + .withForced() + .withEnabled(); +} + +QString GameOblivion::name() const +{ + return "Oblivion Support Plugin"; +} + +QString GameOblivion::localizedName() const +{ + return tr("Oblivion Support Plugin"); +} + +QString GameOblivion::author() const +{ + return "Tannin & MO2 Team"; +} + +QString GameOblivion::description() const +{ + return tr("Adds support for the game Oblivion"); +} + +MOBase::VersionInfo GameOblivion::version() const +{ + return VersionInfo(1, 6, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameOblivion::settings() const +{ + return {PluginSetting("nehrim_downloads", "allow Nehrim downloads", QVariant(false))}; +} + +void GameOblivion::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Oblivion", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/oblivion.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "oblivion_default.ini", + "oblivion.ini"); + } else { + copyToProfile(myGamesPath(), path, "oblivion.ini"); + } + + copyToProfile(myGamesPath(), path, "oblivionprefs.ini"); + } +} + +QString GameOblivion::savegameExtension() const +{ + return "ess"; +} + +QString GameOblivion::savegameSEExtension() const +{ + return "obse"; +} + +std::shared_ptr +GameOblivion::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameOblivion::steamAPPId() const +{ + return "22330"; +} + +QStringList GameOblivion::primaryPlugins() const +{ + return {"oblivion.esm", "update.esm"}; +} + +QString GameOblivion::gameShortName() const +{ + return "Oblivion"; +} + +QStringList GameOblivion::validShortNames() const +{ + QStringList shortNames; + if (m_Organizer->pluginSetting(name(), "nehrim_downloads").toBool()) { + shortNames.append("Nehrim"); + } + return shortNames; +} + +QString GameOblivion::gameNexusName() const +{ + return "Oblivion"; +} + +QStringList GameOblivion::iniFiles() const +{ + return {"oblivion.ini", "oblivionprefs.ini"}; +} + +QStringList GameOblivion::DLCPlugins() const +{ + return {"DLCBattlehornCastle.esp", "DLCShiveringIsles.esp", "Knights.esp", + "DLCFrostcrag.esp", "DLCSpellTomes.esp", "DLCMehrunesRazor.esp", + "DLCOrrery.esp", "DLCThievesDen.esp", "DLCVileLair.esp", + "DLCHorseArmor.esp"}; +} + +int GameOblivion::nexusModOrganizerID() const +{ + return 38277; +} + +int GameOblivion::nexusGameID() const +{ + return 101; +} + +QString GameOblivion::identifyGamePath() const +{ +#ifdef _WIN32 + return GameGamebryo::identifyGamePath(); +#else + // Prefer Steam app-id resolution to avoid matching Oblivion Remastered. + QString path = parseSteamLocation(steamAPPId(), "Oblivion"); + if (!path.isEmpty() && looksValid(QDir(path))) { + return path; + } + + path = MOBase::findSteamGame("Oblivion", "Oblivion.exe"); + if (!path.isEmpty() && looksValid(QDir(path))) { + return path; + } + + return GameGamebryo::identifyGamePath(); +#endif +} diff --git a/libs/game_bethesda/src/games/oblivion/gameoblivion.h b/libs/game_bethesda/src/games/oblivion/gameoblivion.h new file mode 100644 index 0000000..124aa7f --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/gameoblivion.h @@ -0,0 +1,51 @@ +#ifndef GAMEOBLIVION_H +#define GAMEOBLIVION_H + +#include "gamegamebryo.h" + +#include +#include + +class GameOblivion : public GameGamebryo +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.tannin.GameOblivion" FILE "gameoblivion.json") + +public: + GameOblivion(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QString gameShortName() const override; + virtual QStringList validShortNames() const override; + virtual QString gameNexusName() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + +protected: + QString identifyGamePath() const override; + std::shared_ptr makeSaveGame(QString filePath) const override; + QString savegameExtension() const override; + QString savegameSEExtension() const override; +}; + +#endif // GAMEOBLIVION_H diff --git a/libs/game_bethesda/src/games/oblivion/gameoblivion.json b/libs/game_bethesda/src/games/oblivion/gameoblivion.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/gameoblivion.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.cpp b/libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.cpp new file mode 100644 index 0000000..864d37c --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "oblivionbsainvalidation.h" + +OblivionBSAInvalidation::OblivionBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "oblivion.ini", game) +{} + +QString OblivionBSAInvalidation::invalidationBSAName() const +{ + return "Oblivion - Invalidation.bsa"; +} + +unsigned long OblivionBSAInvalidation::bsaVersion() const +{ + return 0x67; +} diff --git a/libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.h b/libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.h new file mode 100644 index 0000000..6804748 --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.h @@ -0,0 +1,20 @@ +#ifndef OBLIVIONBSAINVALIDATION_H +#define OBLIVIONBSAINVALIDATION_H + +#include "gamebryobsainvalidation.h" +#include "obliviondataarchives.h" + +#include + +class OblivionBSAInvalidation : public GamebryoBSAInvalidation +{ +public: + OblivionBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; +}; + +#endif // OBLIVIONBSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/oblivion/obliviondataarchives.cpp b/libs/game_bethesda/src/games/oblivion/obliviondataarchives.cpp new file mode 100644 index 0000000..47c3dd4 --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/obliviondataarchives.cpp @@ -0,0 +1,32 @@ +#include "obliviondataarchives.h" +#include + +QStringList OblivionDataArchives::vanillaArchives() const +{ + return {"Oblivion - Misc.bsa", "Oblivion - Textures - Compressed.bsa", + "Oblivion - Meshes.bsa", "Oblivion - Sounds.bsa", + "Oblivion - Voices1.bsa", "Oblivion - Voices2.bsa"}; +} + +QStringList OblivionDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("oblivion.ini") + : localGameDirectory().absoluteFilePath("oblivion.ini"); + result.append(getArchivesFromKey(iniFile, "SArchiveList")); + + return result; +} + +void OblivionDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("oblivion.ini") + : localGameDirectory().absoluteFilePath("oblivion.ini"); + setArchivesToKey(iniFile, "SArchiveList", list); +} diff --git a/libs/game_bethesda/src/games/oblivion/obliviondataarchives.h b/libs/game_bethesda/src/games/oblivion/obliviondataarchives.h new file mode 100644 index 0000000..380f3ca --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/obliviondataarchives.h @@ -0,0 +1,23 @@ +#ifndef OBLIVIONDATAARCHIVES_H +#define OBLIVIONDATAARCHIVES_H + +#include +#include +#include +#include +#include + +class OblivionDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // OBLIVIONDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/oblivion/oblivionmoddatachecker.cpp b/libs/game_bethesda/src/games/oblivion/oblivionmoddatachecker.cpp new file mode 100644 index 0000000..30a74ee --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/oblivionmoddatachecker.cpp @@ -0,0 +1,30 @@ +#include "oblivionmoddatachecker.h" + +MOBase::ModDataChecker::CheckReturn OblivionModDataChecker::dataLooksValid( + std::shared_ptr fileTree) const +{ + // Check with Gamebryo stuff: + auto check = GamebryoModDataChecker::dataLooksValid(fileTree); + if (check == CheckReturn::VALID) { + return check; + } + + // Check for OBSE_ files: + for (auto const& entry : *fileTree) { + if (entry->isDir() || !entry->name().startsWith("OBSE", Qt::CaseInsensitive)) { + return CheckReturn::INVALID; + } + } + + return CheckReturn::FIXABLE; +} + +std::shared_ptr +OblivionModDataChecker::fix(std::shared_ptr fileTree) const +{ + // If we arrive here, it means all files starts with OBSE. + auto data = fileTree->createOrphanTree(); + auto obse = data->addDirectory("OBSE/Plugins"); + obse->merge(fileTree); + return data; +} diff --git a/libs/game_bethesda/src/games/oblivion/oblivionmoddatachecker.h b/libs/game_bethesda/src/games/oblivion/oblivionmoddatachecker.h new file mode 100644 index 0000000..f62d015 --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/oblivionmoddatachecker.h @@ -0,0 +1,35 @@ +#ifndef OBLIVION_MODATACHECKER_H +#define OBLIVION_MODATACHECKER_H + +#include + +class OblivionModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + + CheckReturn + dataLooksValid(std::shared_ptr fileTree) const override; + std::shared_ptr + fix(std::shared_ptr fileTree) const override; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{"fonts", "interface", "menus", + "meshes", "music", "scripts", + "shaders", "sound", "strings", + "textures", "trees", "video", + "facegen", "obse", "distantlod", + "asi", "distantland", "mits", + "dllplugins", "CalienteTools", "NetScriptFramework"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "bsa", "modgroups", "ini"}; + return result; + } +}; + +#endif // OBLIVION_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/oblivion/oblivionmoddatacontent.h b/libs/game_bethesda/src/games/oblivion/oblivionmoddatacontent.h new file mode 100644 index 0000000..4e412d0 --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/oblivionmoddatacontent.h @@ -0,0 +1,22 @@ +#ifndef OBLIVION_MODDATACONTENT_H +#define OBLIVION_MODDATACONTENT_H + +#include +#include + +class OblivionModDataContent : public GamebryoModDataContent +{ +public: + /** + * + */ + OblivionModDataContent(const MOBase::IGameFeatures* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + // Just need to disable some contents: + m_Enabled[CONTENT_MCM] = false; + m_Enabled[CONTENT_SKYPROC] = false; + } +}; + +#endif // OBLIVION_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/oblivion/oblivionsavegame.cpp b/libs/game_bethesda/src/games/oblivion/oblivionsavegame.cpp new file mode 100644 index 0000000..2f26714 --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/oblivionsavegame.cpp @@ -0,0 +1,74 @@ +#include "oblivionsavegame.h" + +OblivionSaveGame::OblivionSaveGame(QString const& fileName, GameOblivion const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "TES4SAVEGAME"); + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING); + + _SYSTEMTIME creationTime; + fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, + creationTime); + setCreationTime(creationTime); +} + +void OblivionSaveGame::fetchInformationFields(FileWrapper& file, + uint32_t& saveNumber, + QString& playerName, + unsigned short& playerLevel, + QString& playerLocation, + _SYSTEMTIME& creationTime) const +{ + file.skip(); // Major version + file.skip(); // Minor version + + file.skip<_SYSTEMTIME>(); // exe last modified (!) + + file.skip(); // Header version + file.skip(); // Header size + + file.read(saveNumber); + + file.read(playerName); + file.read(playerLevel); + file.read(playerLocation); + + file.skip(); // game days + file.skip(); // game ticks + + // there is a save time stored here. So use it rather than the file time, which + // could have been copied. + // Note: This says it uses getlocaltime api to obtain it which is u/s - if so + // we should ignore this. + file.read(creationTime); +} + +std::unique_ptr OblivionSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TES4SAVEGAME"); + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING); + + std::unique_ptr fields = std::make_unique(); + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + _SYSTEMTIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation, + dummyTime); + } + + // Note that screenshot size, width, height and data are apparently the same + // structure + uint32_t imageSize; + file.read(imageSize); // Screenshot size. + if (imageSize > 0) { + fields->Screenshot = file.readImage(); + } + + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/oblivion/oblivionsavegame.h b/libs/game_bethesda/src/games/oblivion/oblivionsavegame.h new file mode 100644 index 0000000..3961214 --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/oblivionsavegame.h @@ -0,0 +1,21 @@ +#ifndef OBLIVIONSAVEGAME_H +#define OBLIVIONSAVEGAME_H + +#include "gamebryosavegame.h" +#include "gameoblivion.h" + +class OblivionSaveGame : public GamebryoSaveGame +{ +public: + OblivionSaveGame(QString const& fileName, GameOblivion const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& wrapper, uint32_t& saveNumber, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation, _SYSTEMTIME& creationTime) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // OBLIVIONSAVEGAME_H diff --git a/libs/game_bethesda/src/games/oblivion/oblivionscriptextender.cpp b/libs/game_bethesda/src/games/oblivion/oblivionscriptextender.cpp new file mode 100644 index 0000000..f021a2e --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/oblivionscriptextender.cpp @@ -0,0 +1,20 @@ +#include "oblivionscriptextender.h" + +#include +#include + +OblivionScriptExtender::OblivionScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +OblivionScriptExtender::~OblivionScriptExtender() {} + +QString OblivionScriptExtender::BinaryName() const +{ + return "obse_loader.exe"; +} + +QString OblivionScriptExtender::PluginPath() const +{ + return "obse/plugins"; +} diff --git a/libs/game_bethesda/src/games/oblivion/oblivionscriptextender.h b/libs/game_bethesda/src/games/oblivion/oblivionscriptextender.h new file mode 100644 index 0000000..1044bfa --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/oblivionscriptextender.h @@ -0,0 +1,18 @@ +#ifndef OBLIVIONSCRIPTEXTENDER_H +#define OBLIVIONSCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class OblivionScriptExtender : public GamebryoScriptExtender +{ +public: + OblivionScriptExtender(const GameGamebryo* game); + ~OblivionScriptExtender(); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // OBLIVIONSCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/skyrim/CMakeLists.txt b/libs/game_bethesda/src/games/skyrim/CMakeLists.txt new file mode 100644 index 0000000..1fba8d5 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_skyrim SHARED + gameskyrim.cpp + gameskyrim.h + skyrimbsainvalidation.cpp + skyrimbsainvalidation.h + skyrimdataarchives.cpp + skyrimdataarchives.h + skyrimgameplugins.cpp + skyrimgameplugins.h + skyrimmoddatachecker.h + skyrimmoddatacontent.h + skyrimsavegame.cpp + skyrimsavegame.h + skyrimscriptextender.cpp + skyrimscriptextender.h +) +mo2_configure_plugin(game_skyrim NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_skyrim PRIVATE game_gamebryo) +mo2_install_plugin(game_skyrim) diff --git a/libs/game_bethesda/src/games/skyrim/game_skyrim_en.ts b/libs/game_bethesda/src/games/skyrim/game_skyrim_en.ts new file mode 100644 index 0000000..1ba702c --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/game_skyrim_en.ts @@ -0,0 +1,17 @@ + + + + + GameSkyrim + + + Skyrim Support Plugin + + + + + Adds support for the game Skyrim + + + + diff --git a/libs/game_bethesda/src/games/skyrim/gameskyrim.cpp b/libs/game_bethesda/src/games/skyrim/gameskyrim.cpp new file mode 100644 index 0000000..4a0053e --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/gameskyrim.cpp @@ -0,0 +1,263 @@ +#include "gameskyrim.h" + +#include "skyrimbsainvalidation.h" +#include "skyrimdataarchives.h" +#include "skyrimgameplugins.h" +#include "skyrimmoddatachecker.h" +#include "skyrimmoddatacontent.h" +#include "skyrimsavegame.h" +#include "skyrimscriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#ifdef _WIN32 +#include +#include +#endif + +#include +#include +#include +#include +#include + +using namespace MOBase; + +GameSkyrim::GameSkyrim() {} + +bool GameSkyrim::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "skyrim.ini")); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameSkyrim::gameName() const +{ + return "Skyrim"; +} + +QList GameSkyrim::executables() const +{ + return QList() + << ExecutableInfo("SKSE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("SBW", findInGameFolder("SBW.exe")) + << ExecutableInfo("Skyrim", findInGameFolder(binaryName())) + << ExecutableInfo("Skyrim Launcher", findInGameFolder(getLauncherName())) + << ExecutableInfo("BOSS", findInGameFolder("BOSS/BOSS.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Skyrim\"") + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("202480"); +} + +QList GameSkyrim::executableForcedLoads() const +{ + return QList(); +} + +QString GameSkyrim::name() const +{ + return "Skyrim Support Plugin"; +} + +QString GameSkyrim::localizedName() const +{ + return tr("Skyrim Support Plugin"); +} + +QString GameSkyrim::author() const +{ + return "Tannin & MO2 Team"; +} + +QString GameSkyrim::description() const +{ + return tr("Adds support for the game Skyrim"); +} + +MOBase::VersionInfo GameSkyrim::version() const +{ + return VersionInfo(1, 6, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameSkyrim::settings() const +{ + QList results; + results.push_back( + PluginSetting("sse_downloads", "allow Skyrim SE downloads", QVariant(false))); + return results; +} + +void GameSkyrim::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Skyrim", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/skyrim.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "skyrim_default.ini", + "skyrim.ini"); + } else { + copyToProfile(myGamesPath(), path, "skyrim.ini"); + } + + copyToProfile(myGamesPath(), path, "skyrimprefs.ini"); + } +} + +QString GameSkyrim::savegameExtension() const +{ + return "ess"; +} + +QString GameSkyrim::savegameSEExtension() const +{ + return "skse"; +} + +std::shared_ptr GameSkyrim::makeSaveGame(QString filepath) const +{ + return std::make_shared(filepath, this); +} + +QString GameSkyrim::steamAPPId() const +{ + return "72850"; +} + +QStringList GameSkyrim::primaryPlugins() const +{ + return {"skyrim.esm", "update.esm"}; +} + +QString GameSkyrim::binaryName() const +{ + return "TESV.exe"; +} + +QString GameSkyrim::gameShortName() const +{ + return "Skyrim"; +} + +QString GameSkyrim::gameNexusName() const +{ + return "skyrim"; +} + +QStringList GameSkyrim::validShortNames() const +{ + QStringList results; + if (m_Organizer->pluginSetting(name(), "sse_downloads").toBool()) { + results.push_back("SkyrimSE"); + } + return results; +} + +QStringList GameSkyrim::iniFiles() const +{ + return {"skyrim.ini", "skyrimprefs.ini"}; +} + +QStringList GameSkyrim::DLCPlugins() const +{ + return {"Dawnguard.esm", + "Dragonborn.esm", + "HearthFires.esm", + "HighResTexturePack01.esp", + "HighResTexturePack02.esp", + "HighResTexturePack03.esp"}; +} + +namespace +{ +// Note: This is ripped off from shared/util. And in an upcoming move, the fomod +// installer requires something similar. I suspect I should abstract this out +// into gamebryo (or lower level) + +#ifdef _WIN32 +VS_FIXEDFILEINFO GetFileVersion(const std::wstring& fileName) +{ + DWORD handle = 0UL; + DWORD size = ::GetFileVersionInfoSizeW(fileName.c_str(), &handle); + if (size == 0) { + throw std::runtime_error("failed to determine file version info size"); + } + + std::vector buffer(size); + handle = 0UL; + if (!::GetFileVersionInfoW(fileName.c_str(), handle, size, buffer.data())) { + throw std::runtime_error("failed to determine file version info"); + } + + void* versionInfoPtr = nullptr; + UINT versionInfoLength = 0; + if (!::VerQueryValue(buffer.data(), L"\\", &versionInfoPtr, &versionInfoLength)) { + throw std::runtime_error("failed to determine file version"); + } + + return *static_cast(versionInfoPtr); +} +#endif + +} // namespace + +IPluginGame::LoadOrderMechanism GameSkyrim::loadOrderMechanism() const +{ +#ifdef _WIN32 + try { + std::wstring fileName = + gameDirectory().absoluteFilePath(binaryName()).toStdWString().c_str(); + VS_FIXEDFILEINFO versionInfo = ::GetFileVersion(fileName); + if ((versionInfo.dwFileVersionMS > 0x10004) || // version >= 1.5.x? + ((versionInfo.dwFileVersionMS == 0x10004) && + (versionInfo.dwFileVersionLS >= 0x1A0000))) { // version >= ?.4.26 + return LoadOrderMechanism::PluginsTxt; + } + } catch (const std::exception& e) { + qCritical() << "TESV.exe is invalid: " << e.what(); + } +#endif + return LoadOrderMechanism::FileTime; +} + +int GameSkyrim::nexusModOrganizerID() const +{ + return 0; +} + +int GameSkyrim::nexusGameID() const +{ + return 110; +} diff --git a/libs/game_bethesda/src/games/skyrim/gameskyrim.h b/libs/game_bethesda/src/games/skyrim/gameskyrim.h new file mode 100644 index 0000000..2fbc2c4 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/gameskyrim.h @@ -0,0 +1,55 @@ +#ifndef GAMESKYRIM_H +#define GAMESKYRIM_H + +#include "gamegamebryo.h" + +#include +#include + +class GameSkyrim : public GameGamebryo +{ + Q_OBJECT +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.GameSkyrim" FILE "gameskyrim.json") +#endif + +public: + GameSkyrim(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QString binaryName() const override; + virtual QString gameShortName() const override; + virtual QString gameNexusName() const override; + virtual QStringList validShortNames() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + +protected: + virtual QString savegameExtension() const override; + virtual QString savegameSEExtension() const override; + virtual std::shared_ptr + makeSaveGame(QString filepath) const override; +}; + +#endif // GAMESKYRIM_H diff --git a/libs/game_bethesda/src/games/skyrim/gameskyrim.json b/libs/game_bethesda/src/games/skyrim/gameskyrim.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/gameskyrim.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp b/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp new file mode 100644 index 0000000..e056f10 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "skyrimbsainvalidation.h" + +SkyrimBSAInvalidation::SkyrimBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "skyrim.ini", game) +{} + +QString SkyrimBSAInvalidation::invalidationBSAName() const +{ + return "Skyrim - Invalidation.bsa"; +} + +unsigned long SkyrimBSAInvalidation::bsaVersion() const +{ + return 0x68; +} diff --git a/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.h b/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.h new file mode 100644 index 0000000..4937fe9 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.h @@ -0,0 +1,20 @@ +#ifndef SKYRIMBSAINVALIDATION_H +#define SKYRIMBSAINVALIDATION_H + +#include "gamebryobsainvalidation.h" +#include "skyrimdataarchives.h" + +#include + +class SkyrimBSAInvalidation : public GamebryoBSAInvalidation +{ +public: + SkyrimBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; +}; + +#endif // SKYRIMBSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp b/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp new file mode 100644 index 0000000..6422175 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp @@ -0,0 +1,42 @@ +#include "skyrimdataarchives.h" +#include + +QStringList SkyrimDataArchives::vanillaArchives() const +{ + return {"Skyrim - Misc.bsa", "Skyrim - Shaders.bsa", + "Skyrim - Textures.bsa", "HighResTexturePack01.bsa", + "HighResTexturePack02.bsa", "HighResTexturePack03.bsa", + "Skyrim - Interface.bsa", "Skyrim - Animations.bsa", + "Skyrim - Meshes.bsa", "Skyrim - Sounds.bsa", + "Skyrim - Voices.bsa", "Skyrim - VoicesExtra.bsa"}; +} + +QStringList SkyrimDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("skyrim.ini") + : localGameDirectory().absoluteFilePath("skyrim.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + + return result; +} + +void SkyrimDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("skyrim.ini") + : localGameDirectory().absoluteFilePath("skyrim.ini"); + if (list.length() > 255) { + int splitIdx = list.lastIndexOf(",", 256); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.h b/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.h new file mode 100644 index 0000000..b8c2ae0 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.h @@ -0,0 +1,23 @@ +#ifndef SKYRIMDATAARCHIVES_H +#define SKYRIMDATAARCHIVES_H + +#include +#include +#include +#include +#include + +class SkyrimDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // SKYRIMDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/skyrim/skyrimgameplugins.cpp b/libs/game_bethesda/src/games/skyrim/skyrimgameplugins.cpp new file mode 100644 index 0000000..88aac81 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimgameplugins.cpp @@ -0,0 +1,123 @@ +#include "skyrimgameplugins.h" +#include +#include +#include +#include + +#include +#include +#include + +using MOBase::IOrganizer; +using MOBase::IPluginGame; +using MOBase::IPluginList; +using MOBase::reportError; +using MOBase::SafeWriteFile; + +SkyrimGamePlugins::SkyrimGamePlugins(IOrganizer* organizer) + : GamebryoGamePlugins(organizer) +{} + +void SkyrimGamePlugins::readPluginLists(MOBase::IPluginList* pluginList) +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || + QFileInfo(loadOrderPath).lastModified() > m_LastRead; + bool pluginsIsNew = + !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; + + if (pluginsIsNew && !loadOrderIsNew) { + // If the plugins is new but not loadorder, we must reparse the load order from the + // plugin files + + // removed because returned loadorder was incorrect and did not account for plugins + // that were already disabled before. + /*QStringList loadOrder = readPluginList(pluginList); + pluginList->setLoadOrder(loadOrder);*/ + + // Fix me: we are ignoring order changes in plugins.txt favouring loadorder.txt in + // all cases (plugins.txt shuld have precedence) + QStringList loadOrder = readLoadOrderList(pluginList, loadOrderPath); + pluginList->setLoadOrder(loadOrder); + readPluginList(pluginList); + } else { + // read both files if they are both new or both older than the last read + QStringList loadOrder = readLoadOrderList(pluginList, loadOrderPath); + pluginList->setLoadOrder(loadOrder); + readPluginList(pluginList); + } + + m_LastRead = QDateTime::currentDateTime(); +} + +// TODO: return value is incorrect and should be ignored (it's not currently used +QStringList SkyrimGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + QStringList plugins = pluginList->pluginNames(); + QStringList primaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList loadOrder(plugins); + + for (const QString& pluginName : primaryPlugins) { + if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + } + } + + // Do not sort the primary plugins. Their load order should be locked as defined in + // "primaryPlugins". + const QStringList pluginsClone(plugins); + for (QString plugin : pluginsClone) { + if (primaryPlugins.contains(plugin, Qt::CaseInsensitive)) + plugins.removeAll(plugin); + } + + // Determine plugin active state by the plugins.txt file. + bool pluginsTxtExists = true; + QString filePath = organizer()->profile()->absolutePath() + "/plugins.txt"; + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + pluginsTxtExists = false; + } + ON_BLOCK_EXIT([&]() { + qDebug("close %s", qUtf8Printable(filePath)); + file.close(); + }); + + if (file.size() == 0) { + // MO stores at least a header in the file. if it's completely empty the + // file is broken + pluginsTxtExists = false; + } + + if (pluginsTxtExists) { + while (!file.atEnd()) { + QByteArray line = file.readLine(); + QString pluginName; + if ((line.size() > 0) && (line.at(0) != '#')) { + pluginName = QStringEncoder(QStringConverter::Encoding::System) + .encode(line.trimmed().constData()); + } + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + plugins.removeAll(pluginName); + // we already have the old loadorder and we ignore the positions in plugins.txt + // (needs fix) loadOrder.append(pluginName); + } + } + + file.close(); + + // we removed each plugin found in the file, so what's left are inactive mods + for (const QString& pluginName : plugins) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } else { + for (const QString& pluginName : plugins) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + + return loadOrder; +} diff --git a/libs/game_bethesda/src/games/skyrim/skyrimgameplugins.h b/libs/game_bethesda/src/games/skyrim/skyrimgameplugins.h new file mode 100644 index 0000000..8795c91 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimgameplugins.h @@ -0,0 +1,23 @@ +#ifndef _SKYRIMGAMEPLUGINS_H +#define _SKYRIMGAMEPLUGINS_H + +#include +#include +#include +#include + +class SkyrimGamePlugins : public GamebryoGamePlugins +{ +public: + SkyrimGamePlugins(MOBase::IOrganizer* organizer); + + virtual void readPluginLists(MOBase::IPluginList* pluginList) override; + +protected: + virtual QStringList readPluginList(MOBase::IPluginList* pluginList) override; + +private: + std::map m_LastSaveHash; +}; + +#endif // _SKYRIMSEGAMEPLUGINS_H diff --git a/libs/game_bethesda/src/games/skyrim/skyrimmoddatachecker.h b/libs/game_bethesda/src/games/skyrim/skyrimmoddatachecker.h new file mode 100644 index 0000000..4de6d6c --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimmoddatachecker.h @@ -0,0 +1,50 @@ +#ifndef SKYRIM_MODATACHECKER_H +#define SKYRIM_MODATACHECKER_H + +#include + +class SkyrimModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{"fonts", + "interface", + "menus", + "meshes", + "music", + "scripts", + "shaders", + "sound", + "strings", + "textures", + "trees", + "video", + "facegen", + "materials", + "skse", + "distantlod", + "asi", + "Tools", + "MCM", + "distantland", + "mits", + "dllplugins", + "SkyProc Patchers", + "CalienteTools", + "NetScriptFramework", + "shadersfx", + "Nemesis_Engine"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "bsa", "modgroups", "ini"}; + return result; + } +}; + +#endif // SKYRIM_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/skyrim/skyrimmoddatacontent.h b/libs/game_bethesda/src/games/skyrim/skyrimmoddatacontent.h new file mode 100644 index 0000000..96ec5a6 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimmoddatacontent.h @@ -0,0 +1,14 @@ +#ifndef SKYRIM_MODDATACONTENT_H +#define SKYRIM_MODDATACONTENT_H + +#include +#include + +// Skyrim does not need any change from the default feature: +class SkyrimModDataContent : public GamebryoModDataContent +{ +public: + using GamebryoModDataContent::GamebryoModDataContent; +}; + +#endif // SKYRIM_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/skyrim/skyrimsavegame.cpp b/libs/game_bethesda/src/games/skyrim/skyrimsavegame.cpp new file mode 100644 index 0000000..bd16036 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimsavegame.cpp @@ -0,0 +1,105 @@ +#include "skyrimsavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +// SYSTEMTIME is _SYSTEMTIME (defined in gamebryosavegame.h for Linux) +using SYSTEMTIME = _SYSTEMTIME; + +// Portable FileTimeToSystemTime: converts Windows FILETIME to SYSTEMTIME +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; // leftover 100ns units + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); // Qt Mon=1..Sun=7 -> Win Sun=0..Sat=6 + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gameskyrim.h" + +SkyrimSaveGame::SkyrimSaveGame(QString const& fileName, GameSkyrim const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); + + FILETIME ftime; + fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, ftime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&ftime, &ctime); + setCreationTime(ctime); +} + +void SkyrimSaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& saveNumber, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, FILETIME& creationTime) const +{ + file.skip(); // header size + file.skip(); // header version + file.read(saveNumber); + + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + + file.setPluginStringFormat(GamebryoSaveGame::StringFormat::LOCAL8BIT); + file.read(playerLocation); + file.setPluginStringFormat(GamebryoSaveGame::StringFormat::UTF8); + + QString timeOfDay; + file.read(timeOfDay); + + QString race; + file.read(race); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); +} + +std::unique_ptr SkyrimSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); + std::unique_ptr fields = std::make_unique(); + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation, + dummyTime); + } + + fields->Screenshot = file.readImage(); + + file.skip(); // form version + file.skip(); // plugin info size + + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/skyrim/skyrimsavegame.h b/libs/game_bethesda/src/games/skyrim/skyrimsavegame.h new file mode 100644 index 0000000..2df96b2 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimsavegame.h @@ -0,0 +1,28 @@ +#ifndef SKYRIMSAVEGAME_H +#define SKYRIMSAVEGAME_H + +#include "gamebryosavegame.h" +#include "windows_compat.h" + +namespace MOBase +{ +class IPluginGame; +} + +class GameSkyrim; + +class SkyrimSaveGame : public GamebryoSaveGame +{ +public: + SkyrimSaveGame(QString const& fileName, GameSkyrim const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& wrapper, uint32_t& saveNumber, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation, FILETIME& creationTime) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // SKYRIMSAVEGAME_H diff --git a/libs/game_bethesda/src/games/skyrim/skyrimscriptextender.cpp b/libs/game_bethesda/src/games/skyrim/skyrimscriptextender.cpp new file mode 100644 index 0000000..38e17ed --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimscriptextender.cpp @@ -0,0 +1,18 @@ +#include "skyrimscriptextender.h" + +#include +#include + +SkyrimScriptExtender::SkyrimScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString SkyrimScriptExtender::BinaryName() const +{ + return "skse_loader.exe"; +} + +QString SkyrimScriptExtender::PluginPath() const +{ + return "skse/plugins"; +} diff --git a/libs/game_bethesda/src/games/skyrim/skyrimscriptextender.h b/libs/game_bethesda/src/games/skyrim/skyrimscriptextender.h new file mode 100644 index 0000000..6e95249 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimscriptextender.h @@ -0,0 +1,17 @@ +#ifndef SKYRIMSCRIPTEXTENDER_H +#define SKYRIMSCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class SkyrimScriptExtender : public GamebryoScriptExtender +{ +public: + SkyrimScriptExtender(const GameGamebryo* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // SKYRIMSCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/skyrimse/CMakeLists.txt b/libs/game_bethesda/src/games/skyrimse/CMakeLists.txt new file mode 100644 index 0000000..2ec2b79 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(Qt6 COMPONENTS Widgets REQUIRED) + +set(CMAKE_AUTOMOC ON) + +add_library(game_skyrimse SHARED + gameskyrimse.cpp + gameskyrimse.h + skyrimsedataarchives.cpp + skyrimsedataarchives.h + skyrimsemoddatachecker.h + skyrimsemoddatacontent.h + skyrimsesavegame.cpp + skyrimsesavegame.h + skyrimsescriptextender.cpp + skyrimsescriptextender.h + skyrimseunmanagedmods.cpp + skyrimseunmanagedmods.h +) + +target_link_libraries(game_skyrimse PRIVATE game_creation) + +# Keep uibase bare includes after system headers to avoid strings.h collision. +target_compile_options(game_skyrimse PRIVATE + "-idirafter" "${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase" +) + +set_property(TARGET game_skyrimse APPEND PROPERTY AUTOMOC_MOC_OPTIONS + "-I${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase" +) + +# Install the .so plugin next to the organizer binary +install(TARGETS game_skyrimse + LIBRARY DESTINATION plugins +) diff --git a/libs/game_bethesda/src/games/skyrimse/game_skyrimse_en.ts b/libs/game_bethesda/src/games/skyrimse/game_skyrimse_en.ts new file mode 100644 index 0000000..53d72ca --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/game_skyrimse_en.ts @@ -0,0 +1,17 @@ + + + + + GameSkyrimSE + + + Skyrim Special Edition Support Plugin + + + + + Adds support for the game Skyrim Special Edition. + + + + diff --git a/libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp b/libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp new file mode 100644 index 0000000..5452239 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp @@ -0,0 +1,359 @@ +#include "gameskyrimse.h" + +#include "skyrimsedataarchives.h" +#include "skyrimsemoddatachecker.h" +#include "skyrimsemoddatacontent.h" +#include "skyrimsesavegame.h" +#include "skyrimsescriptextender.h" +#include "skyrimseunmanagedmods.h" + +#include "versioninfo.h" +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "scopeguard.h" +#include + +using namespace MOBase; + +GameSkyrimSE::GameSkyrimSE() {} + +void GameSkyrimSE::setVariant(QString variant) +{ + m_GameVariant = variant; +} + +void GameSkyrimSE::checkVariants() +{ + QFileInfo gog_dll(m_GamePath + "/Galaxy64.dll"); + QFileInfo epic_dll(m_GamePath + "/EOSSDK-Win64-Shipping.dll"); + if (gog_dll.exists()) + setVariant("GOG"); + else if (epic_dll.exists()) + setVariant("Epic Games"); + else + setVariant("Steam"); +} + +QDir GameSkyrimSE::documentsDirectory() const +{ + return m_MyGamesPath; +} + +void GameSkyrimSE::detectGame() +{ + m_GamePath = identifyGamePath(); + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QString GameSkyrimSE::identifyGamePath() const +{ +#ifdef _WIN32 + QMap paths = { + {"Software\\Bethesda Softworks\\" + gameName(), "Installed Path"}, + {"Software\\GOG.com\\Games\\1162721350", "path"}, + {"Software\\GOG.com\\Games\\1711230643", "path"}, + }; + + QString result; + for (auto& path : paths.toStdMap()) { + result = findInRegistry(HKEY_LOCAL_MACHINE, path.first.toStdWString().c_str(), + path.second.toStdWString().c_str()); + if (!result.isEmpty()) + break; + } + + // Check Epic Games Manifests + // AppName: ac82db5035584c7f8a2c548d98c86b2c + // AE Update: 5d600e4f59974aeba0259c7734134e27 + if (result.isEmpty()) { + result = parseEpicGamesLocation( + {"ac82db5035584c7f8a2c548d98c86b2c", "5d600e4f59974aeba0259c7734134e27"}); + } + + return result; +#else + // On Linux, use base class NaK detection (handles Steam via game_finder) + QString result = GameGamebryo::identifyGamePath(); + + // Check Epic Games (Heroic/Legendary on Linux) + if (result.isEmpty()) { + result = parseEpicGamesLocation( + {"ac82db5035584c7f8a2c548d98c86b2c", "5d600e4f59974aeba0259c7734134e27"}); + } + + // Try Steam VDF location parsing as fallback + if (result.isEmpty()) { + result = parseSteamLocation("489830", "Skyrim Special Edition"); + } + + return result; +#endif +} + +void GameSkyrimSE::setGamePath(const QString& path) +{ + m_GamePath = path; + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QDir GameSkyrimSE::savesDirectory() const +{ + return QDir(m_MyGamesPath + "/Saves"); +} + +QString GameSkyrimSE::myGamesPath() const +{ + return m_MyGamesPath; +} + +bool GameSkyrimSE::isInstalled() const +{ + return !m_GamePath.isEmpty(); +} + +bool GameSkyrimSE::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "Skyrimcustom.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameSkyrimSE::gameName() const +{ + return "Skyrim Special Edition"; +} + +QString GameSkyrimSE::gameDirectoryName() const +{ + if (selectedVariant() == "GOG") + return "Skyrim Special Edition GOG"; + else if (selectedVariant() == "Epic Games") + return "Skyrim Special Edition EPIC"; + else + return "Skyrim Special Edition"; +} + +QList GameSkyrimSE::executables() const +{ + return QList() + << ExecutableInfo("SKSE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Skyrim Special Edition", findInGameFolder(binaryName())) + << ExecutableInfo("Skyrim Special Edition Launcher", + findInGameFolder(getLauncherName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("1946180") + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Skyrim Special Edition\""); +} + +QList GameSkyrimSE::executableForcedLoads() const +{ + return QList(); +} + +QFileInfo GameSkyrimSE::findInGameFolder(const QString& relativePath) const +{ + return QFileInfo(m_GamePath + "/" + relativePath); +} + +QString GameSkyrimSE::name() const +{ + return "Skyrim Special Edition Support Plugin"; +} + +QString GameSkyrimSE::localizedName() const +{ + return tr("Skyrim Special Edition Support Plugin"); +} + +QString GameSkyrimSE::author() const +{ + return "MO2 Team, Orig: Archost & ZachHaber"; +} + +QString GameSkyrimSE::description() const +{ + return tr("Adds support for the game Skyrim Special Edition."); +} + +MOBase::VersionInfo GameSkyrimSE::version() const +{ + return VersionInfo(1, 7, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameSkyrimSE::settings() const +{ + return {PluginSetting("enderal_downloads", "allow Enderal and Enderal SE downloads", + QVariant(false))}; +} + +void GameSkyrimSE::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/" + gameDirectoryName(), path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/skyrim.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "skyrim_default.ini", + "skyrim.ini"); + } else { + copyToProfile(myGamesPath(), path, "skyrim.ini"); + } + + copyToProfile(myGamesPath(), path, "skyrimprefs.ini"); + copyToProfile(myGamesPath(), path, "skyrimcustom.ini"); + } +} + +QString GameSkyrimSE::savegameExtension() const +{ + return "ess"; +} + +QString GameSkyrimSE::savegameSEExtension() const +{ + return "skse"; +} + +std::shared_ptr +GameSkyrimSE::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameSkyrimSE::steamAPPId() const +{ + if (selectedVariant() == "Steam") + return "489830"; + return QString(); +} + +QStringList GameSkyrimSE::primaryPlugins() const +{ + QStringList plugins = {"skyrim.esm", "update.esm", "dawnguard.esm", "hearthfires.esm", + "dragonborn.esm"}; + + plugins.append(CCPlugins()); + + return plugins; +} + +QStringList GameSkyrimSE::gameVariants() const +{ + return {"Steam", "GOG", "Epic Games"}; +} + +QString GameSkyrimSE::gameShortName() const +{ + return "SkyrimSE"; +} + +QStringList GameSkyrimSE::validShortNames() const +{ + QStringList shortNames{"Skyrim"}; + if (m_Organizer->pluginSetting(name(), "enderal_downloads").toBool()) { + shortNames.append({"Enderal", "EnderalSE"}); + } + return shortNames; +} + +QString GameSkyrimSE::gameNexusName() const +{ + return "skyrimspecialedition"; +} + +QStringList GameSkyrimSE::iniFiles() const +{ + return {"skyrim.ini", "skyrimprefs.ini", "skyrimcustom.ini"}; +} + +QStringList GameSkyrimSE::DLCPlugins() const +{ + return {"dawnguard.esm", "hearthfires.esm", "dragonborn.esm"}; +} + +QStringList GameSkyrimSE::CCPlugins() const +{ + QStringList plugins; + std::set pluginsLookup; + + const QString path = gameDirectory().filePath("Skyrim.ccc"); + + MOBase::forEachLineInFile(path, [&](QString s) { + const auto lc = s.toLower(); + if (!pluginsLookup.contains(lc)) { + pluginsLookup.insert(lc); + plugins.append(std::move(s)); + } + }); + + return plugins; +} + +IPluginGame::LoadOrderMechanism GameSkyrimSE::loadOrderMechanism() const +{ + return IPluginGame::LoadOrderMechanism::PluginsTxt; +} + +int GameSkyrimSE::nexusModOrganizerID() const +{ + return 6194; //... Should be 0? +} + +int GameSkyrimSE::nexusGameID() const +{ + return 1704; // 1704 +} + +QDir GameSkyrimSE::gameDirectory() const +{ + return QDir(m_GamePath); +} + +// Not to delete all the spaces... +MappingType GameSkyrimSE::mappings() const +{ + MappingType result; + + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameDirectoryName() + "/" + profileFile, + false}); + } + + return result; +} diff --git a/libs/game_bethesda/src/games/skyrimse/gameskyrimse.h b/libs/game_bethesda/src/games/skyrimse/gameskyrimse.h new file mode 100644 index 0000000..5296464 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/gameskyrimse.h @@ -0,0 +1,72 @@ +#ifndef _GAMESKYRIMSE_H +#define _GAMESKYRIMSE_H + +#include "gamegamebryo.h" + +#include +#include + +class GameSkyrimSE : public GameGamebryo +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "com.soundcontactstudio.GameSkyrimSE") + +public: + GameSkyrimSE(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual void detectGame() override; + virtual QString gameName() const override; + + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QStringList gameVariants() const override; + virtual QString gameShortName() const override; + virtual QString gameNexusName() const override; + virtual QStringList validShortNames() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual QStringList CCPlugins() const override; + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + + virtual bool isInstalled() const override; + virtual void setGamePath(const QString& path) override; + virtual QDir gameDirectory() const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + virtual MappingType mappings() const override; + +protected: + std::shared_ptr makeSaveGame(QString filePath) const override; + QString savegameExtension() const override; + QString savegameSEExtension() const override; + + QString gameDirectoryName() const; + QDir documentsDirectory() const; + QDir savesDirectory() const; + QFileInfo findInGameFolder(const QString& relativePath) const; + QString myGamesPath() const; + + void checkVariants(); + void setVariant(QString variant); + + virtual QString identifyGamePath() const override; +}; + +#endif // _GAMESKYRIMSE_H diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.cpp b/libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.cpp new file mode 100644 index 0000000..a4b5869 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.cpp @@ -0,0 +1,44 @@ +#include "skyrimsedataarchives.h" + +#include "iprofile.h" +#include + +QStringList SkyrimSEDataArchives::vanillaArchives() const +{ + return {"Skyrim - Textures0.bsa", "Skyrim - Textures1.bsa", "Skyrim - Textures2.bsa", + "Skyrim - Textures3.bsa", "Skyrim - Textures4.bsa", "Skyrim - Textures5.bsa", + "Skyrim - Textures6.bsa", "Skyrim - Textures7.bsa", "Skyrim - Textures8.bsa", + "Skyrim - Meshes0.bsa", "Skyrim - Meshes1.bsa", "Skyrim - Voices_en0.bsa", + "Skyrim - Sounds.bsa", "Skyrim - Interface.bsa", "Skyrim - Animations.bsa", + "Skyrim - Shaders.bsa", "Skyrim - Misc.bsa"}; +} + +QStringList SkyrimSEDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("skyrim.ini") + : localGameDirectory().absoluteFilePath("skyrim.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + + return result; +} + +void SkyrimSEDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("skyrim.ini") + : localGameDirectory().absoluteFilePath("skyrim.ini"); + if (list.length() > 255) { + int splitIdx = list.lastIndexOf(",", 256); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.h b/libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.h new file mode 100644 index 0000000..b3d70c1 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.h @@ -0,0 +1,25 @@ +#ifndef _SKYRIMSEDATAARCHIVES_H +#define _SKYRIMSEDATAARCHIVES_H + +#include "gamebryodataarchives.h" +#include +#include + +namespace MOBase +{ +class IProfile; +} + +class SkyrimSEDataArchives : public GamebryoDataArchives +{ + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // _SKYRIMSEDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimsemoddatachecker.h b/libs/game_bethesda/src/games/skyrimse/skyrimsemoddatachecker.h new file mode 100644 index 0000000..ebcca0f --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimsemoddatachecker.h @@ -0,0 +1,42 @@ +#ifndef SKYRIMSE_MODATACHECKER_H +#define SKYRIMSE_MODATACHECKER_H + +#include + +class SkyrimSEModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{ + + "fonts", "interface", + "menus", "meshes", + "music", "scripts", + "shaders", "sound", + "strings", "textures", + "trees", "video", + "facegen", "materials", + "skse", "distantlod", + "asi", "Tools", + "MCM", "distantland", + "mits", "dllplugins", + "CalienteTools", "NetScriptFramework", + "shadersfx", "Nemesis_Engine", + "Platform", "grass", + "LightPlacer", "mainmenuwallpapers", + "MainMenuVideo", "PBRMaterialObjects", + "PBRNifPatcher", "PBRTextureSets"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "esl", "bsa", "modgroups", "ini"}; + return result; + } +}; + +#endif // SKYRIMSE_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimsemoddatacontent.h b/libs/game_bethesda/src/games/skyrimse/skyrimsemoddatacontent.h new file mode 100644 index 0000000..b252e94 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimsemoddatacontent.h @@ -0,0 +1,21 @@ +#ifndef SKYRIMSE_MODDATACONTENT_H +#define SKYRIMSE_MODDATACONTENT_H + +#include +#include + +class SkyrimSEModDataContent : public GamebryoModDataContent +{ +public: + /** + * + */ + SkyrimSEModDataContent(MOBase::IGameFeatures const* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + // Just need to disable some contents: + m_Enabled[CONTENT_SKYPROC] = false; + } +}; + +#endif // SKYRIMSE_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.cpp b/libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.cpp new file mode 100644 index 0000000..d68ba19 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.cpp @@ -0,0 +1,157 @@ +#include "skyrimsesavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +// Windows ULARGE_INTEGER union for 64-bit FILETIME math +union _ULARGE_INTEGER { + struct { + uint32_t LowPart; + uint32_t HighPart; + }; + uint64_t QuadPart; +}; + +// SYSTEMTIME is _SYSTEMTIME (defined in gamebryosavegame.h for Linux) +using SYSTEMTIME = _SYSTEMTIME; + +// Portable FileTimeToSystemTime: converts Windows FILETIME to SYSTEMTIME +// FILETIME = 100ns intervals since Jan 1, 1601 UTC +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + _ULARGE_INTEGER ull; + ull.LowPart = ft->dwLowDateTime; + ull.HighPart = ft->dwHighDateTime; + + // 11644473600 seconds between 1601-01-01 and 1970-01-01 epochs + int64_t unixSecs = static_cast(ull.QuadPart / 10000000ULL) - 11644473600LL; + uint64_t remainderHns = ull.QuadPart % 10000000ULL; // leftover 100ns units + + QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + QDate d = dt.date(); + QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); // Qt Mon=1..Sun=7 → Win Sun=0..Sat=6 + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +SkyrimSESaveGame::SkyrimSESaveGame(QString const& fileName, GameSkyrimSE const* game) + : GamebryoSaveGame(fileName, game, true) +{ + FileWrapper file(fileName, "TESV_SAVEGAME"); // 10bytes + + uint32_t version; + FILETIME ftime; + fetchInformationFields(file, version, m_PCName, m_PCLevel, m_PCLocation, m_SaveNumber, + ftime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + + // For some reason, the file time is off by about 6 hours. + // So we need to subtract those 6 hours from the filetime. + _ULARGE_INTEGER time; + time.LowPart = ftime.dwLowDateTime; + time.HighPart = ftime.dwHighDateTime; + time.QuadPart -= 216000000000ULL; // 6 hours in 100ns intervals + ftime.dwHighDateTime = time.HighPart; + ftime.dwLowDateTime = time.LowPart; + + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&ftime, &ctime); + + setCreationTime(ctime); +} + +void SkyrimSESaveGame::fetchInformationFields(FileWrapper& file, uint32_t& version, + QString& playerName, + unsigned short& playerLevel, + QString& playerLocation, + uint32_t& saveNumber, + FILETIME& creationTime) const +{ + uint32_t headerSize; + file.read(headerSize); // header size "TESV_SAVEGAME" + file.read(version); + file.read(saveNumber); + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString timeOfDay; + file.read(timeOfDay); + + QString race; + file.read(race); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); // filetime +} + +std::unique_ptr SkyrimSESaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); // 10bytes + + uint32_t version = 0; + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, version, dummyName, dummyLevel, dummyLocation, + dummySaveNumber, dummyTime); + } + + std::unique_ptr fields = std::make_unique(); + + uint32_t width; + uint32_t height; + file.read(width); + file.read(height); + + bool alpha = false; + + // compatibility between LE and SE: + // SE has an additional uin16_t for compression + // SE uses an alpha channel, whereas LE does not + uint16_t compressionType = 0; + if (version == 12) { + file.read(compressionType); + alpha = true; + } + + fields->Screenshot = file.readImage(width, height, 320, alpha); + + file.setCompressionType(compressionType); + file.openCompressedData(); + + uint8_t saveGameVersion = file.readChar(); + uint8_t pluginInfoSize = file.readChar(); + uint16_t other = file.readShort(); // Unknown + + fields->Plugins = file.readPlugins(1); // Just empty data + + if (saveGameVersion >= 78) { + fields->LightPlugins = file.readLightPlugins(); + } + + file.closeCompressedData(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.h b/libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.h new file mode 100644 index 0000000..060b9ee --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.h @@ -0,0 +1,28 @@ +#ifndef _SKYRIMSESAVEGAME_H +#define _SKYRIMSESAVEGAME_H + +#include "gamebryosavegame.h" +#include "gameskyrimse.h" +#include "windows_compat.h" + +namespace MOBase +{ +class IPluginGame; +} + +class SkyrimSESaveGame : public GamebryoSaveGame +{ +public: + SkyrimSESaveGame(QString const& fileName, GameSkyrimSE const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& wrapper, uint32_t& version, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation, uint32_t& saveNumber, + FILETIME& creationTime) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // _SKYRIMSESAVEGAME_H diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.cpp b/libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.cpp new file mode 100644 index 0000000..2dd5cff --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.cpp @@ -0,0 +1,18 @@ +#include "skyrimsescriptextender.h" + +#include +#include + +SkyrimSEScriptExtender::SkyrimSEScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString SkyrimSEScriptExtender::BinaryName() const +{ + return "skse64_loader.exe"; +} + +QString SkyrimSEScriptExtender::PluginPath() const +{ + return "skse/plugins"; +} diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.h b/libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.h new file mode 100644 index 0000000..9b5f78c --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.h @@ -0,0 +1,17 @@ +#ifndef _SKYRIMSESCRIPTEXTENDER_H +#define _SKYRIMSESCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class SkyrimSEScriptExtender : public GamebryoScriptExtender +{ +public: + SkyrimSEScriptExtender(GameGamebryo const* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // _SKYRIMSESCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.cpp b/libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.cpp new file mode 100644 index 0000000..da1fa02 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.cpp @@ -0,0 +1,29 @@ +#include "skyrimseunmanagedmods.h" + +SkyrimSEUnmangedMods::SkyrimSEUnmangedMods(const GameGamebryo* game) + : GamebryoUnmangedMods(game) +{} + +SkyrimSEUnmangedMods::~SkyrimSEUnmangedMods() {} + +QStringList SkyrimSEUnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QDir dataDir(game()->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + + return result; +} diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.h b/libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.h new file mode 100644 index 0000000..27938ca --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.h @@ -0,0 +1,16 @@ +#ifndef _SKYRIMSEUNMANAGEDMODS_H +#define _SKYRIMSEUNMANAGEDMODS_H + +#include "gamebryounmanagedmods.h" +#include + +class SkyrimSEUnmangedMods : public GamebryoUnmangedMods +{ +public: + SkyrimSEUnmangedMods(const GameGamebryo* game); + ~SkyrimSEUnmangedMods(); + + virtual QStringList mods(bool onlyOfficial) const override; +}; + +#endif // _SKYRIMSEUNMANAGEDMODS_H diff --git a/libs/game_bethesda/src/games/skyrimvr/CMakeLists.txt b/libs/game_bethesda/src/games/skyrimvr/CMakeLists.txt new file mode 100644 index 0000000..7554831 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_skyrimvr SHARED + gameskyrimvr.cpp + gameskyrimvr.h + skyrimvrdataarchives.cpp + skyrimvrdataarchives.h + skyrimvrgameplugins.cpp + skyrimvrgameplugins.h + skyrimvrmoddatachecker.h + skyrimvrmoddatacontent.h + skyrimvrsavegame.cpp + skyrimvrsavegame.h + skyrimvrscriptextender.cpp + skyrimvrscriptextender.h + skyrimvrunmanagedmods.cpp + skyrimvrunmanagedmods.h +) +mo2_configure_plugin(game_skyrimvr NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_skyrimvr PRIVATE game_creation) +mo2_install_plugin(game_skyrimvr) diff --git a/libs/game_bethesda/src/games/skyrimvr/game_skyrimvr_en.ts b/libs/game_bethesda/src/games/skyrimvr/game_skyrimvr_en.ts new file mode 100644 index 0000000..064fb57 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/game_skyrimvr_en.ts @@ -0,0 +1,17 @@ + + + + + GameSkyrimVR + + + Skyrim VR Support Plugin + + + + + Adds support for the game Skyrim VR. + + + + diff --git a/libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.cpp b/libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.cpp new file mode 100644 index 0000000..67db6d0 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.cpp @@ -0,0 +1,306 @@ +#include "gameskyrimvr.h" + +#include "skyrimvrdataarchives.h" +#include "skyrimvrgameplugins.h" +#include "skyrimvrmoddatachecker.h" +#include "skyrimvrmoddatacontent.h" +#include "skyrimvrsavegame.h" +#include "skyrimvrscriptextender.h" +#include "skyrimvrunmanagedmods.h" + +#include "versioninfo.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "scopeguard.h" +#include + +using namespace MOBase; + +GameSkyrimVR::GameSkyrimVR() {} + +void GameSkyrimVR::setGamePath(const QString& path) +{ + m_GamePath = path; +} + +QDir GameSkyrimVR::documentsDirectory() const +{ + return m_MyGamesPath; +} + +QString GameSkyrimVR::identifyGamePath() const +{ +#ifdef _WIN32 + QString path = "Software\\Bethesda Softworks\\" + gameName(); + return findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(), + L"Installed Path"); +#else + // Use the shared Linux detection logic from the base class (NaK + fallbacks). + return GameGamebryo::identifyGamePath(); +#endif +} + +QDir GameSkyrimVR::savesDirectory() const +{ + return QDir(m_MyGamesPath + "/Saves"); +} + +QString GameSkyrimVR::myGamesPath() const +{ + return m_MyGamesPath; +} + +bool GameSkyrimVR::isInstalled() const +{ + return !m_GamePath.isEmpty(); +} + +bool GameSkyrimVR::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "SkyrimVR.ini")); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameSkyrimVR::gameName() const +{ + return "Skyrim VR"; +} + +QList GameSkyrimVR::executables() const +{ + return QList() + << ExecutableInfo("SKSE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Skyrim VR", findInGameFolder(binaryName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Skyrim VR\""); +} + +QList GameSkyrimVR::executableForcedLoads() const +{ + return QList(); +} + +QFileInfo GameSkyrimVR::findInGameFolder(const QString& relativePath) const +{ + return QFileInfo(m_GamePath + "/" + relativePath); +} + +QString GameSkyrimVR::name() const +{ + return "Skyrim VR Support Plugin"; +} + +QString GameSkyrimVR::localizedName() const +{ + return tr("Skyrim VR Support Plugin"); +} + +QString GameSkyrimVR::author() const +{ + return "Brixified & MO2 Team"; +} + +QString GameSkyrimVR::description() const +{ + return tr("Adds support for the game Skyrim VR."); +} + +MOBase::VersionInfo GameSkyrimVR::version() const +{ + return VersionInfo(1, 5, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameSkyrimVR::settings() const +{ + return {PluginSetting("enderal_downloads", "allow Enderal and Enderal SE downloads", + QVariant(false))}; +} + +void GameSkyrimVR::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Skyrim VR", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/skyrimvr.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "skyrim.ini", "skyrimvr.ini"); + } else { + copyToProfile(myGamesPath(), path, "skyrimvr.ini"); + } + + copyToProfile(myGamesPath(), path, "skyrimprefs.ini"); + } +} + +QString GameSkyrimVR::savegameExtension() const +{ + return "ess"; +} + +QString GameSkyrimVR::savegameSEExtension() const +{ + return "skse"; +} + +std::shared_ptr +GameSkyrimVR::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameSkyrimVR::steamAPPId() const +{ + return "611670"; +} + +QStringList GameSkyrimVR::primaryPlugins() const +{ + QStringList plugins = {"skyrim.esm", "update.esm", "dawnguard.esm", + "hearthfires.esm", "dragonborn.esm", "skyrimvr.esm"}; + + plugins.append(CCPlugins()); + + return plugins; +} + +QStringList GameSkyrimVR::gameVariants() const +{ + return {"Regular"}; +} + +QString GameSkyrimVR::gameShortName() const +{ + return "SkyrimVR"; +} + +QStringList GameSkyrimVR::primarySources() const +{ + return {"SkyrimSE"}; +} + +QStringList GameSkyrimVR::validShortNames() const +{ + QStringList shortNames{"Skyrim", "SkyrimSE"}; + if (m_Organizer->pluginSetting(name(), "enderal_downloads").toBool()) { + shortNames.append({"Enderal", "EnderalSE"}); + } + return shortNames; +} + +QString GameSkyrimVR::gameNexusName() const +{ + return QString(); +} + +QStringList GameSkyrimVR::iniFiles() const +{ + return {"skyrimvr.ini", "skyrimprefs.ini"}; +} + +QStringList GameSkyrimVR::DLCPlugins() const +{ + return {"dawnguard.esm", "hearthfires.esm", "dragonborn.esm"}; +} + +QStringList GameSkyrimVR::CCPlugins() const +{ + QStringList plugins = {}; + QFile file(gameDirectory().filePath("Skyrim.ccc")); + if (file.open(QIODevice::ReadOnly)) { + ON_BLOCK_EXIT([&file]() { + file.close(); + }); + + if (file.size() == 0) { + return plugins; + } + while (!file.atEnd()) { + QByteArray line = file.readLine().trimmed(); + QString modName; + if ((line.size() > 0) && (line.at(0) != '#')) { + modName = QString::fromUtf8(line.constData()).toLower(); + } + + if (modName.size() > 0) { + if (!plugins.contains(modName, Qt::CaseInsensitive)) { + plugins.append(modName); + } + } + } + } + return plugins; +} + +IPluginGame::LoadOrderMechanism GameSkyrimVR::loadOrderMechanism() const +{ + return IPluginGame::LoadOrderMechanism::PluginsTxt; +} + +MOBase::IPluginGame::SortMechanism GameSkyrimVR::sortMechanism() const +{ + return SortMechanism::LOOT; +} + +int GameSkyrimVR::nexusModOrganizerID() const +{ + return 0; +} + +int GameSkyrimVR::nexusGameID() const +{ + return 0; +} + +QString GameSkyrimVR::getLauncherName() const +{ + return binaryName(); // Skyrim VR has no Launcher, so we just return the name of the + // game binary +} + +QDir GameSkyrimVR::gameDirectory() const +{ + return QDir(m_GamePath); +} + +// Not to delete all the spaces... +MappingType GameSkyrimVR::mappings() const +{ + MappingType result; + + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameName() + "/" + profileFile, false}); + } + + return result; +} diff --git a/libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.h b/libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.h new file mode 100644 index 0000000..b8ec367 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.h @@ -0,0 +1,70 @@ +#ifndef _GAMESKYRIMVR_H +#define _GAMESKYRIMVR_H + +#include "gamegamebryo.h" + +#include +#include + +class GameSkyrimVR : public GameGamebryo +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "com.soundcontactstudio.GameSkyrimVR" FILE "gameskyrimvr.json") + +public: + GameSkyrimVR(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual QString gameName() const override; + + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QStringList gameVariants() const override; + virtual QString gameShortName() const override; + virtual QStringList primarySources() const override; + virtual QStringList validShortNames() const override; + virtual QString gameNexusName() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual QStringList CCPlugins() const override; + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual SortMechanism sortMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + virtual QString getLauncherName() const override; + + virtual bool isInstalled() const override; + virtual void setGamePath(const QString& path) override; + virtual QDir gameDirectory() const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + +public: // IPluginFileMapper + virtual MappingType mappings() const override; + +protected: + std::shared_ptr makeSaveGame(QString filePath) const override; + QString savegameExtension() const override; + QString savegameSEExtension() const override; + + QDir documentsDirectory() const; + QDir savesDirectory() const; + QFileInfo findInGameFolder(const QString& relativePath) const; + QString myGamesPath() const; + virtual QString identifyGamePath() const override; +}; + +#endif // _GAMESKYRIMVR_H diff --git a/libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.json b/libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.cpp b/libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.cpp new file mode 100644 index 0000000..752aa15 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.cpp @@ -0,0 +1,45 @@ +#include "skyrimvrdataarchives.h" + +#include "iprofile.h" +#include + +QStringList SkyrimVRDataArchives::vanillaArchives() const +{ + return {"Skyrim - Textures0.bsa", "Skyrim - Textures1.bsa", "Skyrim - Textures2.bsa", + "Skyrim - Textures3.bsa", "Skyrim - Textures4.bsa", "Skyrim - Textures5.bsa", + "Skyrim - Textures6.bsa", "Skyrim - Textures7.bsa", "Skyrim - Textures8.bsa", + "Skyrim - Meshes0.bsa", "Skyrim - Meshes1.bsa", "Skyrim - Voices_en0.bsa", + "Skyrim - Sounds.bsa", "Skyrim - Interface.bsa", "Skyrim - Animations.bsa", + "Skyrim - Shaders.bsa", "Skyrim - Misc.bsa", "Skyrim - Patch.bsa", + "Skyrim_VR - Main.bsa"}; +} + +QStringList SkyrimVRDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("skyrimvr.ini") + : localGameDirectory().absoluteFilePath("skyrimvr.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + + return result; +} + +void SkyrimVRDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("skyrimvr.ini") + : localGameDirectory().absoluteFilePath("skyrimvr.ini"); + if (list.length() > 255) { + int splitIdx = list.lastIndexOf(",", 256); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.h b/libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.h new file mode 100644 index 0000000..be85d57 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.h @@ -0,0 +1,26 @@ +#ifndef _SKYRIMVRDATAARCHIVES_H +#define _SKYRIMVRDATAARCHIVES_H + +#include "gamebryodataarchives.h" +#include +#include + +namespace MOBase +{ +class IProfile; +} + +class SkyrimVRDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // _SKYRIMVRDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrgameplugins.cpp b/libs/game_bethesda/src/games/skyrimvr/skyrimvrgameplugins.cpp new file mode 100644 index 0000000..1524ffe --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrgameplugins.cpp @@ -0,0 +1,15 @@ +#include "skyrimvrgameplugins.h" + +using namespace MOBase; + +SkyrimVRGamePlugins::SkyrimVRGamePlugins(MOBase::IOrganizer* organizer) + : CreationGamePlugins(organizer) +{} + +bool SkyrimVRGamePlugins::lightPluginsAreSupported() +{ + auto files = m_Organizer->findFiles("skse\\plugins", {"skyrimvresl.dll"}); + if (files.isEmpty()) + return false; + return true; +} diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrgameplugins.h b/libs/game_bethesda/src/games/skyrimvr/skyrimvrgameplugins.h new file mode 100644 index 0000000..6f9676f --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrgameplugins.h @@ -0,0 +1,19 @@ +#ifndef _SKYRIMVRGAMEPLUGINS_H +#define _SKYRIMVRGAMEPLUGINS_H + +#include + +#include +#include + +class SkyrimVRGamePlugins : public CreationGamePlugins +{ + +public: + SkyrimVRGamePlugins(MOBase::IOrganizer* organizer); + +protected: + virtual bool lightPluginsAreSupported() override; +}; + +#endif // _SKYRIMVRGAMEPLUGINS_H diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrmoddatachecker.h b/libs/game_bethesda/src/games/skyrimvr/skyrimvrmoddatachecker.h new file mode 100644 index 0000000..f44d941 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrmoddatachecker.h @@ -0,0 +1,31 @@ +#ifndef SKYRIMVR_MODATACHECKER_H +#define SKYRIMVR_MODATACHECKER_H + +#include + +class SkyrimVRModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{ + "fonts", "interface", "menus", "meshes", + "music", "scripts", "shaders", "sound", + "strings", "textures", "trees", "video", + "facegen", "materials", "skse", "distantlod", + "asi", "Tools", "MCM", "distantland", + "mits", "dllplugins", "CalienteTools", "NetScriptFramework", + "shadersfx", "Nemesis_Engine", "LightPlacer"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "bsa", "modgroups", "ini"}; + return result; + } +}; + +#endif // SKYRIMVR_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrmoddatacontent.h b/libs/game_bethesda/src/games/skyrimvr/skyrimvrmoddatacontent.h new file mode 100644 index 0000000..36f56ff --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrmoddatacontent.h @@ -0,0 +1,21 @@ +#ifndef SKYRIMVR_MODDATACONTENT_H +#define SKYRIMVR_MODDATACONTENT_H + +#include +#include + +class SkyrimVRModDataContent : public GamebryoModDataContent +{ +public: + /** + * + */ + SkyrimVRModDataContent(MOBase::IGameFeatures const* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + // Just need to disable some contents: + m_Enabled[CONTENT_SKYPROC] = false; + } +}; + +#endif // SKYRIMVR_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrsavegame.cpp b/libs/game_bethesda/src/games/skyrimvr/skyrimvrsavegame.cpp new file mode 100644 index 0000000..1670dc8 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrsavegame.cpp @@ -0,0 +1,149 @@ +#include "skyrimvrsavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +// Windows ULARGE_INTEGER union for 64-bit FILETIME math +union _ULARGE_INTEGER { + struct { + uint32_t LowPart; + uint32_t HighPart; + }; + uint64_t QuadPart; +}; + +// SYSTEMTIME is _SYSTEMTIME (defined in gamebryosavegame.h for Linux) +using SYSTEMTIME = _SYSTEMTIME; + +// Portable FileTimeToSystemTime: converts Windows FILETIME to SYSTEMTIME +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; // leftover 100ns units + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); // Qt Mon=1..Sun=7 -> Win Sun=0..Sat=6 + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gameskyrimvr.h" + +SkyrimVRSaveGame::SkyrimVRSaveGame(QString const& fileName, GameSkyrimVR const* game) + : GamebryoSaveGame(fileName, game, true) +{ + FileWrapper file(fileName, "TESV_SAVEGAME"); // 10bytes + + uint32_t version; + FILETIME ftime; + fetchInformationFields(file, version, m_PCName, m_PCLevel, m_PCLocation, m_SaveNumber, + ftime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + + // For some reason, the file time is off by about 6 hours. + // So we need to subtract those 6 hours from the filetime. + _ULARGE_INTEGER time; + time.LowPart = ftime.dwLowDateTime; + time.HighPart = ftime.dwHighDateTime; + time.QuadPart -= 2.16e11; + ftime.dwHighDateTime = time.HighPart; + ftime.dwLowDateTime = time.LowPart; + + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&ftime, &ctime); + + setCreationTime(ctime); +} + +void SkyrimVRSaveGame::fetchInformationFields(FileWrapper& file, uint32_t& version, + QString& playerName, + unsigned short& playerLevel, + QString& playerLocation, + uint32_t& saveNumber, + FILETIME& creationTime) const +{ + uint32_t headerSize; + file.read(headerSize); // header size "TESV_SAVEGAME" + file.read(version); // header version 74 (original Skyrim is 79) + file.read(saveNumber); + + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + + file.read(playerLocation); + + QString timeOfDay; + file.read(timeOfDay); + + QString race; + file.read(race); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); // filetime +} + +std::unique_ptr SkyrimVRSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); // 10bytes + + uint32_t version = 0; + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, version, dummyName, dummyLevel, dummyLocation, + dummySaveNumber, dummyTime); + } + + std::unique_ptr fields = std::make_unique(); + + uint32_t width; + uint32_t height; + file.read(width); + file.read(height); + + uint16_t compressionType; + file.read(compressionType); + file.setCompressionType(compressionType); + + fields->Screenshot = file.readImage(width, height, 320, true); + + file.openCompressedData(); + + uint8_t saveGameVersion = file.readChar(); + uint8_t pluginInfoSize = file.readChar(); + uint16_t other = file.readShort(); // Unknown + + fields->Plugins = file.readPlugins(1); // Just empty data + + if (saveGameVersion >= 78) { + fields->LightPlugins = file.readLightPlugins(); + } + + file.closeCompressedData(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrsavegame.h b/libs/game_bethesda/src/games/skyrimvr/skyrimvrsavegame.h new file mode 100644 index 0000000..28a6ad0 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrsavegame.h @@ -0,0 +1,24 @@ +#ifndef _SKYRIMVRSAVEGAME_H +#define _SKYRIMVRSAVEGAME_H + +#include "gamebryosavegame.h" +#include "windows_compat.h" + +class GameSkyrimVR; + +class SkyrimVRSaveGame : public GamebryoSaveGame +{ +public: + SkyrimVRSaveGame(QString const& fileName, GameSkyrimVR const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& wrapper, uint32_t& version, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation, uint32_t& saveNumber, + FILETIME& creationTime) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // _SKYRIMVRSAVEGAME_H diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrscriptextender.cpp b/libs/game_bethesda/src/games/skyrimvr/skyrimvrscriptextender.cpp new file mode 100644 index 0000000..4d7433d --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrscriptextender.cpp @@ -0,0 +1,18 @@ +#include "skyrimvrscriptextender.h" + +#include +#include + +SkyrimVRScriptExtender::SkyrimVRScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString SkyrimVRScriptExtender::BinaryName() const +{ + return "sksevr_loader.exe"; +} + +QString SkyrimVRScriptExtender::PluginPath() const +{ + return "skse/plugins"; +} diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrscriptextender.h b/libs/game_bethesda/src/games/skyrimvr/skyrimvrscriptextender.h new file mode 100644 index 0000000..9bf3168 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrscriptextender.h @@ -0,0 +1,17 @@ +#ifndef _SKYRIMVRSCRIPTEXTENDER_H +#define _SKYRIMVRSCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class SkyrimVRScriptExtender : public GamebryoScriptExtender +{ +public: + SkyrimVRScriptExtender(GameGamebryo const* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // _SKYRIMVRSCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrunmanagedmods.cpp b/libs/game_bethesda/src/games/skyrimvr/skyrimvrunmanagedmods.cpp new file mode 100644 index 0000000..dd6a158 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrunmanagedmods.cpp @@ -0,0 +1,29 @@ +#include "skyrimvrunmanagedmods.h" + +SkyrimVRUnmangedMods::SkyrimVRUnmangedMods(const GameGamebryo* game) + : GamebryoUnmangedMods(game) +{} + +SkyrimVRUnmangedMods::~SkyrimVRUnmangedMods() {} + +QStringList SkyrimVRUnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QDir dataDir(game()->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + + return result; +} diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrunmanagedmods.h b/libs/game_bethesda/src/games/skyrimvr/skyrimvrunmanagedmods.h new file mode 100644 index 0000000..7b019f9 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrunmanagedmods.h @@ -0,0 +1,16 @@ +#ifndef _SKYRIMVRUNMANAGEDMODS_H +#define _SKYRIMVRUNMANAGEDMODS_H + +#include "gamebryounmanagedmods.h" +#include + +class SkyrimVRUnmangedMods : public GamebryoUnmangedMods +{ +public: + SkyrimVRUnmangedMods(const GameGamebryo* game); + ~SkyrimVRUnmangedMods(); + + virtual QStringList mods(bool onlyOfficial) const override; +}; + +#endif // _SKYRIMVRUNMANAGEDMODS_H diff --git a/libs/game_bethesda/src/games/starfield/CMakeLists.txt b/libs/game_bethesda/src/games/starfield/CMakeLists.txt new file mode 100644 index 0000000..e6b4204 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_starfield SHARED + gamestarfield.cpp + gamestarfield.h + starfieldbsainvalidation.cpp + starfieldbsainvalidation.h + starfielddataarchives.cpp + starfielddataarchives.h + starfieldgameplugins.cpp + starfieldgameplugins.h + starfieldmoddatachecker.h + starfieldmoddatacontent.h + starfieldsavegame.cpp + starfieldsavegame.h + starfieldscriptextender.cpp + starfieldscriptextender.h + starfieldunmanagedmods.cpp + starfieldunmanagedmods.h +) +mo2_configure_plugin(game_starfield NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_starfield PRIVATE game_creation) +mo2_install_plugin(game_starfield) diff --git a/libs/game_bethesda/src/games/starfield/game_starfield_en.ts b/libs/game_bethesda/src/games/starfield/game_starfield_en.ts new file mode 100644 index 0000000..4118e93 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/game_starfield_en.ts @@ -0,0 +1,73 @@ + + + + + GameStarfield + + + Starfield Support Plugin + + + + + Adds support for the game Starfield. + + + + + Show a warning when ESP plugins are enabled in the load order. + + + + + Show a warning when plugins.txt management is invalid. + + + + + You have active ESP plugins in Starfield + + + + + sTestFile entries are present + + + + + <p>ESP plugins are not ideal for Starfield. In addition to being unable to sort them alongside ESM or master-flagged plugins, certain record references are always kept loaded by the game. This consumes unnecessary resources and limits the game's ability to load what it needs.</p><p>Ideally, plugins should be saved as ESM files upon release. It can also be released as an ESL plugin, however there are additional concerns with the way light plugins are currently handled and should only be used when absolutely certain about what you're doing.</p><p>Notably, xEdit does not currently support saving ESP files.</p><h4>Current ESPs:</h4><p>%1</p> + + + + + <p>You have plugin managment enabled but you still have sTestFile settings in your StarfieldCustom.ini. These must be removed or the game will not read the plugins.txt file. Management is still disabled.</p> + + + + + QObject + + + ContentCatalog.txt appears to be corrupt: %1 + + + + + StarfieldModDataContent + + + Materials + + + + + Geometries + + + + + Video + + + + diff --git a/libs/game_bethesda/src/games/starfield/gamestarfield.cpp b/libs/game_bethesda/src/games/starfield/gamestarfield.cpp new file mode 100644 index 0000000..c6c211a --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/gamestarfield.cpp @@ -0,0 +1,438 @@ +#include "gamestarfield.h" + +#include "ipluginlist.h" + +#include "starfieldbsainvalidation.h" +#include "starfielddataarchives.h" +#include "starfieldgameplugins.h" +#include "starfieldmoddatachecker.h" +#include "starfieldmoddatacontent.h" +#include "starfieldsavegame.h" +#include "starfieldscriptextender.h" +#include "starfieldunmanagedmods.h" + +#include "versioninfo.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "utility.h" + +using namespace MOBase; + +const unsigned int GameStarfield::PROBLEM_ESP; +const unsigned int GameStarfield::PROBLEM_TEST_FILE; + +GameStarfield::GameStarfield() {} + +bool GameStarfield::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature( + std::make_shared(this, "StarfieldCustom.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this, localAppFolder())); + registerFeature(std::make_shared(dataArchives.get(), this)); + + return true; +} + +QString GameStarfield::gameName() const +{ + return "Starfield"; +} + +void GameStarfield::detectGame() +{ + m_GamePath = identifyGamePath(); + m_MyGamesPath = determineMyGamesPath("Starfield"); +} + +QString GameStarfield::identifyGamePath() const +{ + return parseSteamLocation(steamAPPId(), gameName()); +} + +QDir GameStarfield::dataDirectory() const +{ + QDir dataDir = documentsDirectory().absoluteFilePath("Data"); + if (!dataDir.exists()) + dataDir.mkdir(dataDir.path()); + return documentsDirectory().absoluteFilePath("Data"); +} + +QMap GameStarfield::secondaryDataDirectories() const +{ + QMap directories; + directories.insert("game_data", gameDirectory().absoluteFilePath("Data")); + return directories; +} + +QList GameStarfield::executables() const +{ + return QList() + << ExecutableInfo("SFSE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Starfield", findInGameFolder(binaryName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("2722710") + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Starfield\""); +} + +QList GameStarfield::executableForcedLoads() const +{ + return QList(); +} + +QString GameStarfield::name() const +{ + return "Starfield Support Plugin"; +} + +QString GameStarfield::localizedName() const +{ + return tr("Starfield Support Plugin"); +} + +QString GameStarfield::author() const +{ + return "Silarn"; +} + +QString GameStarfield::description() const +{ + return tr("Adds support for the game Starfield."); +} + +MOBase::VersionInfo GameStarfield::version() const +{ + return VersionInfo(1, 1, 0, VersionInfo::RELEASE_FINAL); +} + +QList GameStarfield::settings() const +{ + return QList() + << PluginSetting( + "enable_esp_warning", + tr("Show a warning when ESP plugins are enabled in the load order."), + true) + << PluginSetting("enable_management_warnings", + tr("Show a warning when plugins.txt management is invalid."), + true); +} + +MappingType GameStarfield::mappings() const +{ + MappingType result; + if (testFilePlugins().isEmpty()) { + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameShortName() + "/" + profileFile, + false}); + } + } + return result; +} + +void GameStarfield::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Starfield", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + copyToProfile(myGamesPath(), path, "StarfieldPrefs.ini"); + copyToProfile(myGamesPath(), path, "StarfieldCustom.ini"); + } +} + +QString GameStarfield::savegameExtension() const +{ + return "sfs"; +} + +QString GameStarfield::savegameSEExtension() const +{ + return "sfse"; +} + +std::shared_ptr +GameStarfield::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameStarfield::steamAPPId() const +{ + return "1716740"; +} + +QStringList GameStarfield::testFilePlugins() const +{ + QStringList plugins; + if (m_Organizer != nullptr && m_Organizer->profile() != nullptr) { + QString customIni( + m_Organizer->profile()->absoluteIniFilePath("StarfieldCustom.ini")); + if (QFile(customIni).exists()) { + for (int i = 1; i <= 10; ++i) { + QString setting("sTestFile"); + setting += std::to_string(i); + QString plugin = GameGamebryo::readIniValue(customIni, "General", setting, ""); + if (!plugin.isEmpty() && !plugins.contains(plugin, Qt::CaseInsensitive)) { + plugins.append(plugin); + } + } + } + } + return plugins; +} + +QStringList GameStarfield::primaryPlugins() const +{ + QStringList plugins = {"Starfield.esm", "Constellation.esm", + "ShatteredSpace.esm", "OldMars.esm", + "SFBGS003.esm", "SFBGS004.esm", + "SFBGS006.esm", "SFBGS007.esm", + "SFBGS008.esm", "BlueprintShips-Starfield.esm"}; + + for (auto plugin : CCCPlugins()) { + if (!plugins.contains(plugin, Qt::CaseInsensitive)) { + plugins.append(plugin); + } + } + + auto testPlugins = testFilePlugins(); + if (loadOrderMechanism() == LoadOrderMechanism::None) { + plugins << enabledPlugins(); + plugins << testPlugins; + } + + plugins.removeDuplicates(); + + return plugins; +} + +QStringList GameStarfield::enabledPlugins() const +{ + return {}; +} + +QStringList GameStarfield::gameVariants() const +{ + return {"Regular"}; +} + +QString GameStarfield::gameShortName() const +{ + return "Starfield"; +} + +QString GameStarfield::gameNexusName() const +{ + return "starfield"; +} + +QStringList GameStarfield::iniFiles() const +{ + return {"StarfieldPrefs.ini", "StarfieldCustom.ini"}; +} + +bool GameStarfield::prepareIni(const QString& exec) +{ + return true; // no need to write to Starfield.ini +} + +QStringList GameStarfield::DLCPlugins() const +{ + return {"Constellation.esm", "ShatteredSpace.esm"}; +} + +QStringList GameStarfield::CCCPlugins() const +{ + // While the CCC file appears to be mostly legacy, we need to parse it since the game + // will still read it and there are some compatibility reason to use it for + // force-loading the core game plugins. + QStringList plugins = {}; + if (!testFilePresent()) { + QFile myDocsCCCFile(myGamesPath() + "/Starfield.ccc"); + QFile gameCCCFile(gameDirectory().absoluteFilePath("Starfield.ccc")); + QFile* file; + if (myDocsCCCFile.exists()) { + file = &myDocsCCCFile; + } else { + file = &gameCCCFile; + } + if (file->open(QIODevice::ReadOnly)) { + if (file->size() > 0) { + while (!file->atEnd()) { + QByteArray line = file->readLine().trimmed(); + QString modName; + if ((line.size() > 0) && (line.at(0) != '#')) { + modName = QString::fromUtf8(line.constData()); + } + if (modName.size() > 0) { + plugins.append(modName); + } + } + } + } + } + return plugins; +} + +QStringList GameStarfield::CCPlugins() const +{ + QStringList plugins = {}; + std::shared_ptr unmanagedMods = + std::static_pointer_cast( + m_Organizer->gameFeatures()->gameFeature()); + + // The ContentCatalog.txt appears to be the main repository where Starfiled stores + // info about the installed Creations. We parse this to correctly mark unmanaged mods + // as Creations. The StarfieldUnmanagedMods class handles parsing mod names and files. + if (unmanagedMods.get()) { + auto contentCatalog = unmanagedMods->parseContentCatalog(); + for (const auto& mod : contentCatalog) { + if (!plugins.contains(mod.first, Qt::CaseInsensitive)) { + plugins.append(mod.first); + } + } + } + return plugins; +} + +IPluginGame::SortMechanism GameStarfield::sortMechanism() const +{ + return IPluginGame::SortMechanism::LOOT; +} + +IPluginGame::LoadOrderMechanism GameStarfield::loadOrderMechanism() const +{ + if (!testFilePresent()) + return IPluginGame::LoadOrderMechanism::PluginsTxt; + return IPluginGame::LoadOrderMechanism::None; +} + +int GameStarfield::nexusModOrganizerID() const +{ + return 0; +} + +int GameStarfield::nexusGameID() const +{ + return 4187; +} + +// Start Diagnose +std::vector GameStarfield::activeProblems() const +{ + std::vector result; + if (m_Organizer->managedGame() == this) { + if (m_Organizer->pluginSetting(name(), "enable_esp_warning").toBool() && + activeESP()) + result.push_back(PROBLEM_ESP); + if (m_Organizer->pluginSetting(name(), "enable_management_warnings").toBool()) { + if (testFilePresent()) + result.push_back(PROBLEM_TEST_FILE); + } + } + return result; +} + +bool GameStarfield::activeESP() const +{ + m_Active_ESPs.clear(); + std::set enabledPlugins; + + QStringList esps = m_Organizer->findFiles("", [](const QString& fileName) -> bool { + return fileName.endsWith(".esp", FileNameComparator::CaseSensitivity); + }); + + for (const QString& esp : esps) { + QString baseName = QFileInfo(esp).fileName(); + if (m_Organizer->pluginList()->state(baseName) == IPluginList::STATE_ACTIVE) { + m_Active_ESPs.insert(baseName); + } + } + + if (!m_Active_ESPs.empty()) + return true; + return false; +} + +bool GameStarfield::testFilePresent() const +{ + if (!testFilePlugins().isEmpty()) + return true; + return false; +} + +QString GameStarfield::shortDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_ESP: + return tr("You have active ESP plugins in Starfield"); + case PROBLEM_TEST_FILE: + return tr("sTestFile entries are present"); + } + return ""; +} + +QString GameStarfield::fullDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_ESP: { + QString espInfo = SetJoin(m_Active_ESPs, ", "); + return tr("

ESP plugins are not ideal for Starfield. In addition to being unable " + "to sort them alongside ESM or master-flagged plugins, certain record " + "references are always kept loaded by the game. This consumes " + "unnecessary resources and limits the game's ability to load what it " + "needs.

" + "

Ideally, plugins should be saved as ESM files upon release. It can " + "also be released as an ESL plugin, however there are additional " + "concerns with the way light plugins are currently handled and should " + "only be used when absolutely certain about what you're doing.

" + "

Notably, xEdit does not currently support saving ESP files.

" + "

Current ESPs:

%1

") + .arg(espInfo); + } + case PROBLEM_TEST_FILE: { + return tr("

You have plugin managment enabled but you still have sTestFile " + "settings in your StarfieldCustom.ini. These must be removed or the game " + "will not read the plugins.txt file. Management is still disabled.

"); + } + } + return ""; +} + +bool GameStarfield::hasGuidedFix(unsigned int key) const +{ + return false; +} + +void GameStarfield::startGuidedFix(unsigned int key) const {} diff --git a/libs/game_bethesda/src/games/starfield/gamestarfield.h b/libs/game_bethesda/src/games/starfield/gamestarfield.h new file mode 100644 index 0000000..f9724c0 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/gamestarfield.h @@ -0,0 +1,83 @@ +#ifndef GAMESTARFIELD_H +#define GAMESTARFIELD_H + +#include "gamegamebryo.h" +#include "iplugindiagnose.h" + +#include +#include + +class GameStarfield : public GameGamebryo, public MOBase::IPluginDiagnose +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginGame MOBase::IPluginDiagnose) + Q_PLUGIN_METADATA(IID "org.modorganizer.GameStarfield" FILE "gamestarfield.json") + +public: + GameStarfield(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: + QStringList testFilePlugins() const; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual void detectGame() override; + virtual QDir dataDirectory() const override; + virtual QMap secondaryDataDirectories() const override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QStringList enabledPlugins() const override; + virtual QStringList gameVariants() const override; + virtual QString gameShortName() const override; + virtual QString gameNexusName() const override; + virtual QStringList iniFiles() const override; + virtual bool prepareIni(const QString& exec) override; + virtual QStringList DLCPlugins() const override; + virtual QStringList CCPlugins() const override; + virtual SortMechanism sortMechanism() const override; + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + +public: // IPluginDiagnose interface + virtual std::vector activeProblems() const override; + virtual QString shortDescription(unsigned int key) const override; + virtual QString fullDescription(unsigned int key) const override; + virtual bool hasGuidedFix(unsigned int key) const override; + virtual void startGuidedFix(unsigned int key) const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + virtual MappingType mappings() const override; + +protected: + virtual QString identifyGamePath() const override; + std::shared_ptr makeSaveGame(QString filePath) const override; + QString savegameExtension() const override; + QString savegameSEExtension() const override; + +private: + QStringList CCCPlugins() const; + bool activeESP() const; + bool testFilePresent() const; + +private: + static const unsigned int PROBLEM_ESP = 1; + static const unsigned int PROBLEM_TEST_FILE = 2; + + mutable std::set m_Active_ESPs; +}; + +#endif // GAMEStarfield_H diff --git a/libs/game_bethesda/src/games/starfield/gamestarfield.json b/libs/game_bethesda/src/games/starfield/gamestarfield.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/gamestarfield.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.cpp b/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.cpp new file mode 100644 index 0000000..f6e803b --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.cpp @@ -0,0 +1,68 @@ +#include "starfieldbsainvalidation.h" + +#include "dummybsa.h" +#include "iplugingame.h" +#include "iprofile.h" +#include "registry.h" +#include +#include + +#include "gamegamebryo.h" + +StarfieldBSAInvalidation::StarfieldBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "StarfieldCustom.ini", game) +{ + m_IniFileName = "StarfieldCustom.ini"; + m_Game = game; +} + +bool StarfieldBSAInvalidation::isInvalidationBSA(const QString& bsaName) +{ + return false; +} + +QString StarfieldBSAInvalidation::invalidationBSAName() const +{ + return ""; +} + +unsigned long StarfieldBSAInvalidation::bsaVersion() const +{ + return 0x68; +} + +bool StarfieldBSAInvalidation::prepareProfile(MOBase::IProfile* profile) +{ + bool dirty = false; + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : m_Game->documentsDirectory().absolutePath(); + QString iniFilePath = basePath + "/" + m_IniFileName; + + if (profile->invalidationActive(nullptr)) { + // write bInvalidateOlderFiles = 1, if needed + QString bInvalidateOlderFiles = GameGamebryo::readIniValue( + iniFilePath, "Archive", "bInvalidateOlderFiles", "0"); + if (bInvalidateOlderFiles.toLong() != 1) { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "bInvalidateOlderFiles", "1", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + QString sResourceDataDirsFinal = GameGamebryo::readIniValue( + iniFilePath, "Archive", "sResourceDataDirsFinal", "STRINGS\\"); + if (sResourceDataDirsFinal != "") { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "sResourceDataDirsFinal", "", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } + + return dirty; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.h b/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.h new file mode 100644 index 0000000..40ec355 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.h @@ -0,0 +1,33 @@ +#ifndef STARFIELDBSAINVALIDATION_H +#define STARFIELDBSAINVALIDATION_H + +#include "gamebryobsainvalidation.h" +#include "starfielddataarchives.h" +#include +#include + +#include + +namespace MOBase +{ +class IPluginGame; +} + +class StarfieldBSAInvalidation : public GamebryoBSAInvalidation +{ +public: + StarfieldBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + virtual bool isInvalidationBSA(const QString& bsaName) override; + virtual bool prepareProfile(MOBase::IProfile* profile) override; + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; + +private: + QString m_IniFileName; + MOBase::IPluginGame const* m_Game; +}; + +#endif // STARFIELDBSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/starfield/starfielddataarchives.cpp b/libs/game_bethesda/src/games/starfield/starfielddataarchives.cpp new file mode 100644 index 0000000..d7eadae --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfielddataarchives.cpp @@ -0,0 +1,98 @@ +#include "starfielddataarchives.h" + +#include "iprofile.h" +#include + +QStringList StarfieldDataArchives::vanillaArchives() const +{ + return {"Starfield - Animations.ba2", + "Starfield - DensityMaps.ba2", + "Starfield - FaceAnimation01.ba2", + "Starfield - FaceAnimation02.ba2", + "Starfield - FaceAnimation03.ba2", + "Starfield - FaceAnimation04.ba2", + "Starfield - FaceAnimationPatch.ba2", + "Starfield - FaceMeshes.ba2", + "Starfield - GeneratedTextures.ba2", + "Starfield - Interface.ba2", + "Starfield - Localization.ba2", + "Starfield - LODMeshes.ba2", + "Starfield - LODMeshesPatch.ba2", + "Starfield - LODTextures.ba2", + "Starfield - Materials.ba2", + "Starfield - Meshes01.ba2", + "Starfield - Meshes02.ba2", + "Starfield - MeshesPatch.ba2", + "Starfield - Misc.ba2", + "Starfield - Particles.ba2", + "Starfield - ParticlesTestData.ba2", + "Starfield - PlanetData.ba2", + "Starfield - Shaders.ba2", + "Starfield - ShadersBeta.ba2", + "Starfield - Terrain01.ba2", + "Starfield - Terrain02.ba2", + "Starfield - Terrain03.ba2", + "Starfield - Terrain04.ba2", + "Starfield - TerrainPatch.ba2", + "Starfield - Textures01.ba2", + "Starfield - Textures02.ba2", + "Starfield - Textures03.ba2", + "Starfield - Textures04.ba2", + "Starfield - Textures05.ba2", + "Starfield - Textures06.ba2", + "Starfield - Textures07.ba2", + "Starfield - Textures08.ba2", + "Starfield - Textures09.ba2", + "Starfield - Textures10.ba2", + "Starfield - Textures11.ba2", + "Starfield - TexturesPatch.ba2", + "Starfield - Voices01.ba2", + "Starfield - Voices02.ba2", + "Starfield - VoicesPatch.ba2", + "Starfield - WwiseSounds01.ba2", + "Starfield - WwiseSounds02.ba2", + "Starfield - WwiseSounds03.ba2", + "Starfield - WwiseSounds04.ba2", + "Starfield - WwiseSounds05.ba2", + "Starfield - WwiseSoundsPatch.ba2", + "Constellation - Localization.ba2", + "Constellation - Textures.ba2", + "OldMars - Localization.ba2", + "OldMars - Textures.ba2", + "BlueprintShips-Starfield - Localization.ba2"}; +} + +QStringList StarfieldDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString defaultIniFile = gameDirectory().absoluteFilePath("Starfield.ini"); + QString customIniFile = + profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("StarfieldCustom.ini") + : localGameDirectory().absoluteFilePath("StarfieldCustom.ini"); + QStringList archiveSettings = {"SResourceArchiveList", "sResourceIndexFileList", + "SResourceArchiveMemoryCacheList", + "sResourceStartUpArchiveList", + "sResourceEnglishVoiceList"}; + for (auto setting : archiveSettings) { + auto archives = getArchivesFromKey(customIniFile, setting, 4096); + if (archives.isEmpty()) + archives = getArchivesFromKey(defaultIniFile, setting, 4096); + result.append(archives); + } + + return result; +} + +void StarfieldDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{} + +void StarfieldDataArchives::addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) +{} + +void StarfieldDataArchives::removeArchive(MOBase::IProfile* profile, + const QString& archiveName) +{} diff --git a/libs/game_bethesda/src/games/starfield/starfielddataarchives.h b/libs/game_bethesda/src/games/starfield/starfielddataarchives.h new file mode 100644 index 0000000..c83943f --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfielddataarchives.h @@ -0,0 +1,28 @@ +#ifndef STARFIELDDATAARCHIVES_H +#define STARFIELDDATAARCHIVES_H + +#include +#include + +#include "gamebryodataarchives.h" + +class GameGamebryo; + +class StarfieldDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + virtual void addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) override; + virtual void removeArchive(MOBase::IProfile* profile, + const QString& archiveName) override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // STARFIELDDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp b/libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp new file mode 100644 index 0000000..6b4e6ab --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp @@ -0,0 +1,35 @@ +#include "starfieldgameplugins.h" + +using namespace MOBase; + +StarfieldGamePlugins::StarfieldGamePlugins(MOBase::IOrganizer* organizer) + : CreationGamePlugins(organizer) +{} + +bool StarfieldGamePlugins::mediumPluginsAreSupported() +{ + return true; +} + +bool StarfieldGamePlugins::blueprintPluginsAreSupported() +{ + return true; +} + +void StarfieldGamePlugins::writePluginList(const IPluginList* pluginList, + const QString& filePath) +{ + if (m_Organizer->managedGame()->loadOrderMechanism() == + MOBase::IPluginGame::LoadOrderMechanism::PluginsTxt) { + CreationGamePlugins::writePluginList(pluginList, filePath); + } +} + +QStringList StarfieldGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + if (m_Organizer->managedGame()->loadOrderMechanism() == + MOBase::IPluginGame::LoadOrderMechanism::PluginsTxt) { + return CreationGamePlugins::readPluginList(pluginList); + } + return {}; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldgameplugins.h b/libs/game_bethesda/src/games/starfield/starfieldgameplugins.h new file mode 100644 index 0000000..a957b13 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldgameplugins.h @@ -0,0 +1,23 @@ +#ifndef _STARFIELDGAMEPLUGINS_H +#define _STARFIELDGAMEPLUGINS_H + +#include + +#include +#include + +class StarfieldGamePlugins : public CreationGamePlugins +{ + +public: + StarfieldGamePlugins(MOBase::IOrganizer* organizer); + +protected: + virtual bool mediumPluginsAreSupported() override; + virtual bool blueprintPluginsAreSupported() override; + virtual void writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) override; + virtual QStringList readPluginList(MOBase::IPluginList* pluginList) override; +}; + +#endif // _STARFIELDGAMEPLUGINS_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldmoddatachecker.h b/libs/game_bethesda/src/games/starfield/starfieldmoddatachecker.h new file mode 100644 index 0000000..13c1fcc --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldmoddatachecker.h @@ -0,0 +1,29 @@ +#ifndef STARFIELD_MODATACHECKER_H +#define STARFIELD_MODATACHECKER_H + +#include + +class StarfieldModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{ + "interface", "meshes", "geometries", "music", "scripts", "sound", + "strings", "textures", "trees", "video", "materials", "sfse", + "distantlod", "asi", "Tools", "MCM", "distantland", "mits", + "dllplugins", "CalienteTools", "shadersfx", "aaf"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "esl", "ba2", + "modgroups", "ini", "csg", "cdx"}; + return result; + } +}; + +#endif // STARFIELD_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldmoddatacontent.h b/libs/game_bethesda/src/games/starfield/starfieldmoddatacontent.h new file mode 100644 index 0000000..d4b8014 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldmoddatacontent.h @@ -0,0 +1,55 @@ +#ifndef STARFIELD_MODDATACONTENT_H +#define STARFIELD_MODDATACONTENT_H + +#include +#include + +class StarfieldModDataContent : public GamebryoModDataContent +{ +protected: + enum StarfieldContent + { + CONTENT_MATERIAL = CONTENT_NEXT_VALUE, + CONTENT_GEOMETRIES = CONTENT_NEXT_VALUE + 1, + CONTENT_VIDEO = CONTENT_NEXT_VALUE + 2 + }; + +public: + StarfieldModDataContent(MOBase::IGameFeatures const* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + m_Enabled[CONTENT_SKYPROC] = false; + } + + std::vector getAllContents() const override + { + static std::vector STARFIELD_CONTENTS{ + {CONTENT_MATERIAL, QT_TR_NOOP("Materials"), ":/MO/gui/content/material"}, + {CONTENT_GEOMETRIES, QT_TR_NOOP("Geometries"), ":/MO/gui/content/geometries"}, + {CONTENT_VIDEO, QT_TR_NOOP("Video"), ":/MO/gui/content/media"}}; + auto contents = GamebryoModDataContent::getAllContents(); + std::copy(std::begin(STARFIELD_CONTENTS), std::end(STARFIELD_CONTENTS), + std::back_inserter(contents)); + return contents; + } + + std::vector + getContentsFor(std::shared_ptr fileTree) const override + { + auto contents = GamebryoModDataContent::getContentsFor(fileTree); + for (auto e : *fileTree) { + if (e->isDir()) { + if (e->compare("materials") == 0) { + contents.push_back(CONTENT_MATERIAL); + } else if (e->compare("geometries") == 0) { + contents.push_back(CONTENT_GEOMETRIES); + } else if (e->compare("video") == 0) { + contents.push_back(CONTENT_VIDEO); + } + } + } + return contents; + } +}; + +#endif // STARFIELD_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldsavegame.cpp b/libs/game_bethesda/src/games/starfield/starfieldsavegame.cpp new file mode 100644 index 0000000..49653cb --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldsavegame.cpp @@ -0,0 +1,157 @@ +#include "starfieldsavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gamestarfield.h" + +StarfieldSaveGame::StarfieldSaveGame(QString const& fileName, GameStarfield const* game) + : GamebryoSaveGame(fileName, game, true, true) +{ + FileWrapper file(getFilepath(), "BCPS"); + + getData(file); + FILETIME creationTime; + unsigned char saveVersion; + fetchInformationFields(file, m_SaveNumber, saveVersion, m_PCName, m_PCLevel, + m_PCLocation, creationTime); + file.closeCompressedData(); + file.close(); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&creationTime, &ctime); + + setCreationTime(ctime); +} + +void StarfieldSaveGame::getData(FileWrapper& file) const +{ + file.skip(); // header version + file.skip(); // chunk compressed size array start location + file.skip(); // unknown (0?) + file.setCompressionType(1); + /* + * Parse following variables then begin decompressing data + * - 64-bit int = compressed data start location + * - 64-bit int = complete uncompressed data size + */ + file.openCompressedData(); + /* + * Remaining headers before start of compressed data: + * - 32-bit float (version? appears to be 2.0) + * - 64-bit int - size of uncompressed chunks (250 KiB) + * - 64-bit int - size of byte rows? (16 bytes) used to determine start of each + * compressed chunk + * - 32-bit int - number of chunks? + * - 'ZIP ' - denotes start of chunk compressed size array + * - compressed size array - array of 32-bit ints containing the compressed size of + * each compressed chunk (see number of chunks above) + */ +} + +void StarfieldSaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& saveNumber, unsigned char& saveVersion, + QString& playerName, unsigned short& playerLevel, QString& playerLocation, + FILETIME& creationTime) const +{ + char fileID[12]; // SFS_SAVEGAME + unsigned int headerSize; + unsigned int version; + // file.read(fileID, 12); + headerSize = file.readInt(12); + version = file.readInt(); + saveVersion = file.readChar(); + saveNumber = file.readInt(); + file.read(playerName); + + unsigned int temp; + temp = file.readInt(); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString ignore; + file.read(ignore); // playtime as ascii hh.mm.ss + file.read(ignore); // race name (i.e. BretonRace) + + unsigned short gender; + gender = file.readShort(); // Player gender (0 = male) + float experience, experienceRequired; + experience = file.readFloat(); + experienceRequired = file.readFloat(); + + unsigned long long time = file.readLong(); + creationTime.dwLowDateTime = (DWORD)time; + creationTime.dwHighDateTime = time >> 32; +} + +std::unique_ptr StarfieldSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "BCPS"); // 10bytes + + getData(file); + FILETIME creationTime; + unsigned char saveVersion; + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, saveVersion, dummyName, dummyLevel, + dummyLocation, dummyTime); + } + + int extraInfo = 0; + if (saveVersion >= 122) + extraInfo = 1; + if (saveVersion >= 140) + extraInfo = 2; + QStringList gamePlugins = m_Game->primaryPlugins() + m_Game->enabledPlugins(); + + QString ignore; + std::unique_ptr fields = std::make_unique(); + + file.readChar(12); + file.read(ignore); // game version + file.read(ignore); // game version again? + file.readInt(); // plugin info size + + fields->Plugins = file.readPlugins(0, extraInfo, gamePlugins); + fields->LightPlugins = file.readLightPlugins(0, extraInfo, gamePlugins); + if (saveVersion >= 122) + fields->MediumPlugins = file.readMediumPlugins(0, extraInfo, gamePlugins); + file.closeCompressedData(); + file.close(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldsavegame.h b/libs/game_bethesda/src/games/starfield/starfieldsavegame.h new file mode 100644 index 0000000..7bd07e0 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldsavegame.h @@ -0,0 +1,27 @@ +#ifndef STARFIELDSAVEGAME_H +#define STARFIELDSAVEGAME_H + +#include "gamebryosavegame.h" +#include "windows_compat.h" +#include + +class GameStarfield; + +class StarfieldSaveGame : public GamebryoSaveGame +{ +public: + StarfieldSaveGame(QString const& fileName, GameStarfield const* game); + +protected: + // Fetch easy-to-access information. + void getData(FileWrapper& file) const; + + void fetchInformationFields(FileWrapper& file, uint32_t& saveNumber, + unsigned char& saveVersion, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, + FILETIME& creationTime) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // STARFIELDSAVEGAME_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldscriptextender.cpp b/libs/game_bethesda/src/games/starfield/starfieldscriptextender.cpp new file mode 100644 index 0000000..15cf6cd --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldscriptextender.cpp @@ -0,0 +1,18 @@ +#include "starfieldscriptextender.h" + +#include +#include + +StarfieldScriptExtender::StarfieldScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString StarfieldScriptExtender::BinaryName() const +{ + return "sfse_loader.exe"; +} + +QString StarfieldScriptExtender::PluginPath() const +{ + return "sfse/plugins"; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldscriptextender.h b/libs/game_bethesda/src/games/starfield/starfieldscriptextender.h new file mode 100644 index 0000000..b7c27f1 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldscriptextender.h @@ -0,0 +1,17 @@ +#ifndef STARFIELDSCRIPTEXTENDER_H +#define STARFIELDSCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class StarfieldScriptExtender : public GamebryoScriptExtender +{ +public: + StarfieldScriptExtender(GameGamebryo const* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // STARFIELDSCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp new file mode 100644 index 0000000..30abb38 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp @@ -0,0 +1,132 @@ +#include "starfieldunmanagedmods.h" + +#include "log.h" + +#include +#include +#include +#include + +StarfieldUnmanagedMods::StarfieldUnmanagedMods(const GameStarfield* game, + const QString& appDataFolder) + : GamebryoUnmangedMods(game), m_AppDataFolder(appDataFolder) +{} + +StarfieldUnmanagedMods::~StarfieldUnmanagedMods() {} + +QStringList StarfieldUnmanagedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins() + game()->enabledPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QMap directories = {{"data", game()->dataDirectory()}}; + directories.insert(game()->secondaryDataDirectories()); + for (QDir directory : directories) { + for (const QString& fileName : directory.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + } + + return result; +} + +QFileInfo StarfieldUnmanagedMods::referenceFile(const QString& modName) const +{ + QFileInfoList files; + QMap directories = {{"data", game()->dataDirectory()}}; + directories.insert(game()->secondaryDataDirectories()); + for (QDir directory : directories) { + files += directory.entryInfoList(QStringList() << modName + ".es*"); + } + if (files.size() > 0) { + return files.at(0); + } else { + return QFileInfo(); + } +} + +std::map +StarfieldUnmanagedMods::parseContentCatalog() const +{ + QFile content(m_AppDataFolder + "/" + game()->gameShortName() + + "/ContentCatalog.txt"); + std::map contentCatalog; + if (content.open(QIODevice::OpenModeFlag::ReadOnly)) { + auto contentData = content.readAll(); + QString convertedData = QString::fromLatin1(contentData); + contentData = convertedData.toUtf8(); + QJsonParseError jsonError; + QJsonDocument contentDoc = QJsonDocument::fromJson(contentData, &jsonError); + if (jsonError.error) { + MOBase::log::warn(QObject::tr("ContentCatalog.txt appears to be corrupt: %1") + .arg(jsonError.errorString())); + } else { + QJsonObject contentObj = contentDoc.object(); + for (const auto& mod : contentObj.keys()) { + if (mod == "ContentCatalog") + continue; + auto modInfo = contentObj.value(mod).toObject(); + QStringList pluginList; + QStringList files; + for (const auto& file : modInfo.value("Files").toArray()) { + QString fileName = file.toString(); + files.append(fileName); + if (fileName.endsWith(".esm", Qt::CaseInsensitive) || + fileName.endsWith(".esl", Qt::CaseInsensitive) || + fileName.endsWith(".esp", Qt::CaseInsensitive)) { + pluginList.append(fileName); + } + } + for (const auto& plugin : pluginList) { + contentCatalog[plugin] = ContentCatalog(); + contentCatalog[plugin].files = files; + contentCatalog[plugin].name = modInfo.value("Title").toString(); + } + } + } + } + return contentCatalog; +} + +QStringList StarfieldUnmanagedMods::secondaryFiles(const QString& modName) const +{ + QStringList files; + auto contentCatalog = parseContentCatalog(); + for (const auto& mod : contentCatalog) { + if (mod.first.startsWith(modName, Qt::CaseInsensitive)) { + files += mod.second.files; + break; + } + } + // file extension in FO4 is .ba2 instead of bsa + QMap directories = {{"data", game()->dataDirectory()}}; + directories.insert(game()->secondaryDataDirectories()); + for (QDir directory : directories) { + for (const QString& archiveName : directory.entryList({modName + "*.ba2"})) { + files.append(directory.absoluteFilePath(archiveName)); + } + } + return files; +} + +QString StarfieldUnmanagedMods::displayName(const QString& modName) const +{ + auto contentCatalog = parseContentCatalog(); + for (const auto& mod : contentCatalog) { + if (mod.first.startsWith(modName, Qt::CaseInsensitive)) { + return mod.second.name; + } + } + // unlike in earlier games, in fallout 4 the file name doesn't correspond to + // the public name + return modName; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.h b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.h new file mode 100644 index 0000000..abe14ef --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.h @@ -0,0 +1,36 @@ +#ifndef STARFIELDUNMANAGEDMODS_H +#define STARFIELDUNMANAGEDMODS_H + +#include "gamebryounmanagedmods.h" +#include "gamestarfield.h" +#include + +#include + +class StarfieldUnmanagedMods : public GamebryoUnmangedMods +{ + friend class GameStarfield; + + struct ContentCatalog + { + QString name; + QStringList files; + }; + +public: + StarfieldUnmanagedMods(const GameStarfield* game, const QString& appDataFolder); + ~StarfieldUnmanagedMods(); + + virtual QStringList mods(bool onlyOfficial) const override; + virtual QFileInfo referenceFile(const QString& modName) const override; + virtual QStringList secondaryFiles(const QString& modName) const override; + virtual QString displayName(const QString& modName) const override; + +private: + std::map parseContentCatalog() const; + +private: + QString m_AppDataFolder; +}; + +#endif // STARFIELDUNMANAGEDMODS_H diff --git a/libs/game_bethesda/src/games/ttw/CMakeLists.txt b/libs/game_bethesda/src/games/ttw/CMakeLists.txt new file mode 100644 index 0000000..27149ec --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_ttw SHARED + falloutttwbsainvalidation.cpp + falloutttwbsainvalidation.h + falloutttwdataarchives.cpp + falloutttwdataarchives.h + falloutttwmoddatachecker.h + falloutttwmoddatacontent.h + falloutttwsavegame.cpp + falloutttwsavegame.h + falloutttwscriptextender.cpp + falloutttwscriptextender.h + gamefalloutttw.cpp + gamefalloutttw.h +) +mo2_configure_plugin(game_ttw NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_ttw PRIVATE game_gamebryo) +mo2_install_plugin(game_ttw) diff --git a/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp b/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp new file mode 100644 index 0000000..67a7817 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "falloutttwbsainvalidation.h" + +FalloutTTWBSAInvalidation::FalloutTTWBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "fallout.ini", game) +{} + +QString FalloutTTWBSAInvalidation::invalidationBSAName() const +{ + return "Fallout - Invalidation.bsa"; +} + +unsigned long FalloutTTWBSAInvalidation::bsaVersion() const +{ + return 0x68; +} diff --git a/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.h b/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.h new file mode 100644 index 0000000..96062d3 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.h @@ -0,0 +1,20 @@ +#ifndef FALLOUTTTWBSAINVALIDATION_H +#define FALLOUTTTWBSAINVALIDATION_H + +#include "falloutttwdataarchives.h" +#include "gamebryobsainvalidation.h" + +#include + +class FalloutTTWBSAInvalidation : public GamebryoBSAInvalidation +{ +public: + FalloutTTWBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; +}; + +#endif // FALLOUTTTWBSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp b/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp new file mode 100644 index 0000000..3ffb076 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp @@ -0,0 +1,31 @@ +#include "falloutttwdataarchives.h" +#include + +QStringList FalloutTTWDataArchives::vanillaArchives() const +{ + return {"Fallout - Textures.bsa", "Fallout - Textures2.bsa", "Fallout - Meshes.bsa", + "Fallout - Voices1.bsa", "Fallout - Sound.bsa", "Fallout - Misc.bsa"}; +} + +QStringList FalloutTTWDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini") + : localGameDirectory().absoluteFilePath("fallout.ini"); + result.append(getArchivesFromKey(iniFile, "SArchiveList")); + + return result; +} + +void FalloutTTWDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini") + : localGameDirectory().absoluteFilePath("fallout.ini"); + setArchivesToKey(iniFile, "SArchiveList", list); +} diff --git a/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.h b/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.h new file mode 100644 index 0000000..eb9d7c7 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.h @@ -0,0 +1,23 @@ +#ifndef FALLOUTTTWDATAARCHIVES_H +#define FALLOUTTTWDATAARCHIVES_H + +#include +#include +#include +#include +#include + +class FalloutTTWDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // FALLOUTTTWDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/ttw/falloutttwmoddatachecker.h b/libs/game_bethesda/src/games/ttw/falloutttwmoddatachecker.h new file mode 100644 index 0000000..12ed881 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwmoddatachecker.h @@ -0,0 +1,51 @@ +#ifndef FALLOUTTTW_MODATACHECKER_H +#define FALLOUTTTW_MODATACHECKER_H + +#include + +class FalloutTTWModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{"fonts", + "interface", + "menus", + "meshes", + "music", + "scripts", + "shaders", + "sound", + "strings", + "textures", + "trees", + "video", + "facegen", + "materials", + "nvse", + "distantlod", + "asi", + "Tools", + "MCM", + "distantland", + "mits", + "dllplugins", + "CalienteTools", + "shadersfx", + "config", + "KEYWORDS", + "BaseObjectSwapper", + "RaceMenuPresets"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "bsa", "modgroups", "ini"}; + return result; + } +}; + +#endif // FALLOUTTTW_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/ttw/falloutttwmoddatacontent.h b/libs/game_bethesda/src/games/ttw/falloutttwmoddatacontent.h new file mode 100644 index 0000000..37f9d20 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwmoddatacontent.h @@ -0,0 +1,22 @@ +#ifndef FALLOUTTTW_MODDATACONTENT_H +#define FALLOUTTTW_MODDATACONTENT_H + +#include +#include + +class FalloutTTWModDataContent : public GamebryoModDataContent +{ +public: + /** + * + */ + FalloutTTWModDataContent(MOBase::IGameFeatures const* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + // Just need to disable some contents: + m_Enabled[CONTENT_MCM] = false; + m_Enabled[CONTENT_SKYPROC] = false; + } +}; + +#endif // FALLOUTTTW_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/ttw/falloutttwsavegame.cpp b/libs/game_bethesda/src/games/ttw/falloutttwsavegame.cpp new file mode 100644 index 0000000..9f41082 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwsavegame.cpp @@ -0,0 +1,79 @@ +#include "falloutttwsavegame.h" + +#include "gamefalloutttw.h" + +FalloutTTWSaveGame::FalloutTTWSaveGame(QString const& fileName, + GameFalloutTTW const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "FO3SAVEGAME"); + uint32_t width, height; + fetchInformationFields(file, width, height, m_SaveNumber, m_PCName, m_PCLevel, + m_PCLocation); +} + +void FalloutTTWSaveGame::fetchInformationFields(FileWrapper& file, uint32_t& width, + uint32_t& height, + uint32_t& saveNumber, + QString& playerName, + unsigned short& playerLevel, + QString& playerLocation) const +{ + file.skip(); // Save header size + + file.skip(); // File version? + file.skip(); // Delimiter + + // A huge wodge of text with no length but a delimiter. Given the null bytes + // in it I presume it's fixed length (64 bytes + delim) but I have no + // definite spec + for (unsigned char ignore = 0; ignore != 0x7c;) { + file.read(ignore); // unknown + } + + file.setHasFieldMarkers(true); + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BZSTRING); + + file.read(width); + file.read(height); + file.read(saveNumber); + file.read(playerName); + + QString whatthis; + file.read(whatthis); + + long level; + file.read(level); + playerLevel = level; + file.read(playerLocation); +} + +std::unique_ptr +FalloutTTWSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "FO3SAVEGAME"); + + std::unique_ptr fields = std::make_unique(); + + uint32_t width, height; + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + + fetchInformationFields(file, width, height, dummySaveNumber, dummyName, dummyLevel, + dummyLocation); + } + + QString playtime; + file.read(playtime); + + fields->Screenshot = file.readImage(width, height, 256); + + file.skip(5); // unknown (1 byte), plugin size (4 bytes) + + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING); + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/ttw/falloutttwsavegame.h b/libs/game_bethesda/src/games/ttw/falloutttwsavegame.h new file mode 100644 index 0000000..f8460b3 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwsavegame.h @@ -0,0 +1,23 @@ +#ifndef FALLOUTTTWSAVEGAME_H +#define FALLOUTTTWSAVEGAME_H + +#include "gamebryosavegame.h" + +class GameFalloutTTW; + +class FalloutTTWSaveGame : public GamebryoSaveGame +{ +public: + FalloutTTWSaveGame(QString const& fileName, GameFalloutTTW const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& wrapper, uint32_t& width, + uint32_t& height, uint32_t& saveNumber, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // FALLOUTTTWSAVEGAME_H diff --git a/libs/game_bethesda/src/games/ttw/falloutttwscriptextender.cpp b/libs/game_bethesda/src/games/ttw/falloutttwscriptextender.cpp new file mode 100644 index 0000000..ab9474e --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwscriptextender.cpp @@ -0,0 +1,18 @@ +#include "falloutttwscriptextender.h" + +#include +#include + +FalloutTTWScriptExtender::FalloutTTWScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString FalloutTTWScriptExtender::BinaryName() const +{ + return "nvse_loader.exe"; +} + +QString FalloutTTWScriptExtender::PluginPath() const +{ + return "nvse/plugins"; +} diff --git a/libs/game_bethesda/src/games/ttw/falloutttwscriptextender.h b/libs/game_bethesda/src/games/ttw/falloutttwscriptextender.h new file mode 100644 index 0000000..d1e4c9a --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwscriptextender.h @@ -0,0 +1,17 @@ +#ifndef FALLOUTTTWSCRIPTEXTENDER_H +#define FALLOUTTTWSCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class FalloutTTWScriptExtender : public GamebryoScriptExtender +{ +public: + FalloutTTWScriptExtender(const GameGamebryo* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // FALLOUTTTWSCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/ttw/game_ttw_en.ts b/libs/game_bethesda/src/games/ttw/game_ttw_en.ts new file mode 100644 index 0000000..9d967f9 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/game_ttw_en.ts @@ -0,0 +1,22 @@ + + + + + GameFalloutTTW + + + Fallout TTW Support Plugin + + + + + Adds support for the game Fallout TTW + + + + + While not recommended by the TTW modding community, enables LOOT sorting + + + + diff --git a/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp b/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp new file mode 100644 index 0000000..34eb1b2 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp @@ -0,0 +1,364 @@ +#include "gamefalloutttw.h" + +#include "falloutttwbsainvalidation.h" +#include "falloutttwdataarchives.h" +#include "falloutttwmoddatachecker.h" +#include "falloutttwmoddatacontent.h" +#include "falloutttwsavegame.h" +#include "falloutttwscriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" +#include "versioninfo.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace MOBase; + +GameFalloutTTW::GameFalloutTTW() {} + +bool GameFalloutTTW::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature( + std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "fallout.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +void GameFalloutTTW::setVariant(QString variant) +{ + m_GameVariant = variant; +} + +void GameFalloutTTW::checkVariants() +{ + QFileInfo gog_dll(m_GamePath + "\\Galaxy.dll"); + QFileInfo epic_dll(m_GamePath + "\\EOSSDK-Win32-Shipping.dll"); + if (gog_dll.exists()) + setVariant("GOG"); + else if (epic_dll.exists()) + setVariant("Epic Games"); + else + setVariant("Steam"); +} + +QDir GameFalloutTTW::documentsDirectory() const +{ + return m_MyGamesPath; +} + +QString GameFalloutTTW::identifyGamePath() const +{ + QString result; +#ifdef _WIN32 + QString path = "Software\\Bethesda Softworks\\FalloutNV"; + result = findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(), + L"Installed Path"); +#else + result = GameGamebryo::identifyGamePath(); +#endif + // EPIC Game Store + if (result.isEmpty()) { + /** + * Basegame: 5daeb974a22a435988892319b3a4f476 + * Dead Money: b290229eb58045cbab9501640f3278f3 + * Honest Hearts: 562d4a2c1b3147b089a7c453e3ddbcbe + * Old World Blues: c8dae1ab0570475a8b38a9041e614840 + * Lonesome Road: 4fa3d8d9b2cb4714a19a38d1a598be8f + * Gun Runners' Arsenal: 7dcfb9cd9d134728b2646466c34c7b3b + * Courier's Stash: ee9a44b4530942499ef1c8c390731fce + */ + result = parseEpicGamesLocation({"5daeb974a22a435988892319b3a4f476"}); + if (QFileInfo(result).isDir()) { + QDir startPath = QDir(result); + auto subDirs = startPath.entryList({"Fallout New Vegas*"}, + QDir::Dirs | QDir::NoDotAndDotDot); + if (!subDirs.isEmpty()) + result += "/" + subDirs.first(); + } + } + return result; +} + +void GameFalloutTTW::setGamePath(const QString& path) +{ + m_GamePath = path; + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QDir GameFalloutTTW::savesDirectory() const +{ + return QDir(m_MyGamesPath + "/Saves"); +} + +QString GameFalloutTTW::myGamesPath() const +{ + return m_MyGamesPath; +} + +bool GameFalloutTTW::isInstalled() const +{ + return !m_GamePath.isEmpty(); +} + +QString GameFalloutTTW::gameName() const +{ + return "TTW"; +} + +QString GameFalloutTTW::displayGameName() const +{ + return "Tale of Two Wastelands"; +} + +QString GameFalloutTTW::gameDirectoryName() const +{ + if (selectedVariant() == "Epic Games") + return "FalloutNV_Epic"; + else + return "FalloutNV"; +} + +void GameFalloutTTW::detectGame() +{ + m_GamePath = identifyGamePath(); + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QList GameFalloutTTW::executables() const +{ + ExecutableInfo game("Tale of Two Wastelands", findInGameFolder(binaryName())); + ExecutableInfo launcher("Fallout Launcher", findInGameFolder(getLauncherName())); + QList extraExecutables = + QList() << ExecutableInfo("GECK", findInGameFolder("geck.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"FalloutNV\""); + if (selectedVariant() != "Epic Games") { + extraExecutables.prepend(ExecutableInfo( + "NVSE", findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName()))); + } else { + game.withArgument("-EpicPortal"); + launcher.withArgument("-EpicPortal"); + } + QList executables = {game, launcher}; + executables += extraExecutables; + return executables; +} + +QList GameFalloutTTW::executableForcedLoads() const +{ + return QList(); +} + +QString GameFalloutTTW::name() const +{ + return "Fallout TTW Support Plugin"; +} + +QString GameFalloutTTW::localizedName() const +{ + return tr("Fallout TTW Support Plugin"); +} + +QString GameFalloutTTW::author() const +{ + return "SuperSandro2000 & MO2 Team"; +} + +QString GameFalloutTTW::description() const +{ + return tr("Adds support for the game Fallout TTW"); +} + +MOBase::VersionInfo GameFalloutTTW::version() const +{ + return VersionInfo(1, 6, 0, VersionInfo::RELEASE_FINAL); +} + +QList GameFalloutTTW::settings() const +{ + return QList() + << PluginSetting("enable_loot_sorting", + tr("While not recommended by the TTW modding community, " + "enables LOOT sorting"), + false); +} + +void GameFalloutTTW::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/" + gameDirectoryName(), path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/fallout.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "fallout_default.ini", + "fallout.ini"); + } else { + copyToProfile(myGamesPath(), path, "fallout.ini"); + } + + copyToProfile(myGamesPath(), path, "falloutprefs.ini"); + copyToProfile(myGamesPath(), path, "falloutcustom.ini"); + copyToProfile(myGamesPath(), path, "GECKCustom.ini"); + copyToProfile(myGamesPath(), path, "GECKPrefs.ini"); + } +} + +QString GameFalloutTTW::savegameExtension() const +{ + return "fos"; +} + +QString GameFalloutTTW::savegameSEExtension() const +{ + return "nvse"; +} + +std::shared_ptr +GameFalloutTTW::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameFalloutTTW::steamAPPId() const +{ + if (selectedVariant() == "Steam") { + if (m_GamePath.endsWith("enplczru")) { + return "22490"; + } else { + return "22380"; + } + } + return QString(); +} + +QStringList GameFalloutTTW::primaryPlugins() const +{ + return {"falloutnv.esm", "deadmoney.esm", "honesthearts.esm", + "oldworldblues.esm", "lonesomeroad.esm", "gunrunnersarsenal.esm", + "fallout3.esm", "anchorage.esm", "thepitt.esm", + "brokensteel.esm", "pointlookout.esm", "zeta.esm", + "caravanpack.esm", "classicpack.esm", "mercenarypack.esm", + "tribalpack.esm", "taleoftwowastelands.esm", "YUPTTW.esm"}; +} + +QStringList GameFalloutTTW::gameVariants() const +{ + return {"Steam", "GOG", "Epic Games"}; +} + +QString GameFalloutTTW::binaryName() const +{ + return "FalloutNV.exe"; +} + +QString GameFalloutTTW::gameShortName() const +{ + return "TTW"; +} + +QStringList GameFalloutTTW::primarySources() const +{ + return {"FalloutNV"}; +} + +QStringList GameFalloutTTW::validShortNames() const +{ + return {"FalloutNV", "Fallout3"}; +} + +QString GameFalloutTTW::gameNexusName() const +{ + return ""; +} + +QStringList GameFalloutTTW::iniFiles() const +{ + return {"fallout.ini", "falloutprefs.ini", "falloutcustom.ini", "GECKCustom.ini", + "GECKPrefs.ini"}; +} + +QStringList GameFalloutTTW::DLCPlugins() const +{ + return {}; +} + +MOBase::IPluginGame::SortMechanism GameFalloutTTW::sortMechanism() const +{ + if (m_Organizer->pluginSetting(name(), "enable_loot_sorting").toBool()) + return IPluginGame::SortMechanism::LOOT; + return IPluginGame::SortMechanism::NONE; +} + +QString GameFalloutTTW::lootGameName() const +{ + return "FalloutNV"; +} + +int GameFalloutTTW::nexusModOrganizerID() const +{ + return 0; +} + +int GameFalloutTTW::nexusGameID() const +{ + return 0; +} + +QDir GameFalloutTTW::gameDirectory() const +{ + return QDir(m_GamePath); +} + +QString GameFalloutTTW::getLauncherName() const +{ + return "FalloutNVLauncher.exe"; +} + +MappingType GameFalloutTTW::mappings() const +{ + MappingType result; + + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/FalloutNV/" + profileFile, false}); + if (selectedVariant() == "Epic Games") { + result.push_back( + {m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameDirectoryName() + "/" + profileFile, false}); + } + } + return result; +} diff --git a/libs/game_bethesda/src/games/ttw/gamefalloutttw.h b/libs/game_bethesda/src/games/ttw/gamefalloutttw.h new file mode 100644 index 0000000..7f989b0 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/gamefalloutttw.h @@ -0,0 +1,77 @@ +#ifndef GAMEFALLOUTTTW_H +#define GAMEFALLOUTTTW_H + +#include "gamegamebryo.h" + +#include +#include + +class GameFalloutTTW : public GameGamebryo +{ + Q_OBJECT +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.GameFalloutTTW" FILE "gamefalloutttw.json") +#endif + +public: + GameFalloutTTW(); + + bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual QString displayGameName() const override; + virtual void detectGame() override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QStringList gameVariants() const override; + virtual QString binaryName() const override; + virtual QString gameShortName() const override; + virtual QStringList validShortNames() const override; + virtual QString gameNexusName() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + virtual QStringList primarySources() const override; + virtual SortMechanism sortMechanism() const override; + virtual QString lootGameName() const override; + virtual QString getLauncherName() const override; + + virtual bool isInstalled() const override; + virtual void setGamePath(const QString& path) override; + virtual QDir gameDirectory() const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + +public: // IPluginFileMapper interface + virtual MappingType mappings() const override; + +protected: + QString gameDirectoryName() const; + QDir documentsDirectory() const; + QDir savesDirectory() const; + QString myGamesPath() const; + + void setVariant(QString variant); + void checkVariants(); + +protected: + virtual QString identifyGamePath() const override; + virtual QString savegameExtension() const override; + virtual QString savegameSEExtension() const override; + std::shared_ptr makeSaveGame(QString filePath) const override; +}; + +#endif // GAMEFALLOUTTTW_H diff --git a/libs/game_bethesda/src/games/ttw/gamefalloutttw.json b/libs/game_bethesda/src/games/ttw/gamefalloutttw.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/gamefalloutttw.json @@ -0,0 +1 @@ +{} -- cgit v1.3.1