aboutsummaryrefslogtreecommitdiff
path: root/libs/game_bethesda
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/game_bethesda
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 <noreply@anthropic.com>
Diffstat (limited to 'libs/game_bethesda')
-rw-r--r--libs/game_bethesda/.clang-format41
-rw-r--r--libs/game_bethesda/.git-blame-ignore-revs15
-rw-r--r--libs/game_bethesda/.gitattributes7
-rw-r--r--libs/game_bethesda/.github/workflows/build.yml20
-rw-r--r--libs/game_bethesda/.github/workflows/linting.yml16
-rw-r--r--libs/game_bethesda/.gitignore5
-rw-r--r--libs/game_bethesda/.pre-commit-config.yaml20
-rw-r--r--libs/game_bethesda/CMakeLists.txt56
-rw-r--r--libs/game_bethesda/CMakePresets.json57
-rw-r--r--libs/game_bethesda/src/creation/CMakeLists.txt33
-rw-r--r--libs/game_bethesda/src/creation/creationgameplugins.cpp183
-rw-r--r--libs/game_bethesda/src/creation/creationgameplugins.h22
-rw-r--r--libs/game_bethesda/src/creation/game_creation_en.ts12
-rw-r--r--libs/game_bethesda/src/gamebryo/CMakeLists.txt78
-rw-r--r--libs/game_bethesda/src/gamebryo/dummybsa.cpp198
-rw-r--r--libs/game_bethesda/src/gamebryo/dummybsa.h59
-rw-r--r--libs/game_bethesda/src/gamebryo/game_gamebryo_en.ts171
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.cpp165
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.h36
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryodataarchives.cpp68
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryodataarchives.h37
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryogameplugins.cpp246
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryogameplugins.h38
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp138
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.h54
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.cpp76
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.h46
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.cpp116
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.h75
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp607
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryosavegame.h256
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.cpp100
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.h23
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.cpp199
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.h30
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.ui209
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryoscriptextender.cpp46
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryoscriptextender.h31
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.cpp51
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.h26
-rw-r--r--libs/game_bethesda/src/gamebryo/gamegamebryo.cpp823
-rw-r--r--libs/game_bethesda/src/gamebryo/gamegamebryo.h169
-rw-r--r--libs/game_bethesda/src/gamebryo/vdf_parser.h728
-rw-r--r--libs/game_bethesda/src/games/enderal/CMakeLists.txt24
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp16
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalbsainvalidation.h20
-rw-r--r--libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp42
-rw-r--r--libs/game_bethesda/src/games/enderal/enderaldataarchives.h23
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp123
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalgameplugins.h23
-rw-r--r--libs/game_bethesda/src/games/enderal/enderallocalsavegames.cpp24
-rw-r--r--libs/game_bethesda/src/games/enderal/enderallocalsavegames.h35
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalmoddatachecker.h49
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalmoddatacontent.h14
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalsavegame.cpp101
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalsavegame.h23
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp18
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalscriptextender.h17
-rw-r--r--libs/game_bethesda/src/games/enderal/game_enderal_en.ts17
-rw-r--r--libs/game_bethesda/src/games/enderal/gameenderal.cpp267
-rw-r--r--libs/game_bethesda/src/games/enderal/gameenderal.h61
-rw-r--r--libs/game_bethesda/src/games/enderal/gameenderal.json1
-rw-r--r--libs/game_bethesda/src/games/enderalse/CMakeLists.txt24
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp64
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsedataarchives.h26
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp82
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsegameplugins.h19
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp24
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.h17
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsemoddatachecker.h31
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsemoddatacontent.h21
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp148
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsesavegame.h28
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp18
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsescriptextender.h17
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp29
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.h16
-rw-r--r--libs/game_bethesda/src/games/enderalse/game_enderalse_en.ts25
-rw-r--r--libs/game_bethesda/src/games/enderalse/gameenderalse.cpp347
-rw-r--r--libs/game_bethesda/src/games/enderalse/gameenderalse.h75
-rw-r--r--libs/game_bethesda/src/games/enderalse/gameenderalse.json1
-rw-r--r--libs/game_bethesda/src/games/fallout3/CMakeLists.txt20
-rw-r--r--libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp16
-rw-r--r--libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.h20
-rw-r--r--libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp33
-rw-r--r--libs/game_bethesda/src/games/fallout3/fallout3dataarchives.h20
-rw-r--r--libs/game_bethesda/src/games/fallout3/fallout3moddatachecker.h31
-rw-r--r--libs/game_bethesda/src/games/fallout3/fallout3moddatacontent.h22
-rw-r--r--libs/game_bethesda/src/games/fallout3/fallout3savegame.cpp72
-rw-r--r--libs/game_bethesda/src/games/fallout3/fallout3savegame.h23
-rw-r--r--libs/game_bethesda/src/games/fallout3/fallout3scriptextender.cpp18
-rw-r--r--libs/game_bethesda/src/games/fallout3/fallout3scriptextender.h17
-rw-r--r--libs/game_bethesda/src/games/fallout3/game_fallout3_en.ts17
-rw-r--r--libs/game_bethesda/src/games/fallout3/gamefallout3.cpp279
-rw-r--r--libs/game_bethesda/src/games/fallout3/gamefallout3.h58
-rw-r--r--libs/game_bethesda/src/games/fallout3/gamefallout3.json1
-rw-r--r--libs/game_bethesda/src/games/fallout4/CMakeLists.txt22
-rw-r--r--libs/game_bethesda/src/games/fallout4/fallout4.qrc5
-rw-r--r--libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.cpp68
-rw-r--r--libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.h33
-rw-r--r--libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp48
-rw-r--r--libs/game_bethesda/src/games/fallout4/fallout4dataarchives.h27
-rw-r--r--libs/game_bethesda/src/games/fallout4/fallout4moddatachecker.h29
-rw-r--r--libs/game_bethesda/src/games/fallout4/fallout4moddatacontent.h44
-rw-r--r--libs/game_bethesda/src/games/fallout4/fallout4savegame.cpp106
-rw-r--r--libs/game_bethesda/src/games/fallout4/fallout4savegame.h23
-rw-r--r--libs/game_bethesda/src/games/fallout4/fallout4scriptextender.cpp18
-rw-r--r--libs/game_bethesda/src/games/fallout4/fallout4scriptextender.h17
-rw-r--r--libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.cpp63
-rw-r--r--libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.h18
-rw-r--r--libs/game_bethesda/src/games/fallout4/game_fallout4_en.ts28
-rw-r--r--libs/game_bethesda/src/games/fallout4/gamefallout4.cpp334
-rw-r--r--libs/game_bethesda/src/games/fallout4/gamefallout4.h73
-rw-r--r--libs/game_bethesda/src/games/fallout4/gamefallout4.json1
-rw-r--r--libs/game_bethesda/src/games/fallout4/splash.pngbin0 -> 52981 bytes
-rw-r--r--libs/game_bethesda/src/games/fallout4london/CMakeLists.txt20
-rw-r--r--libs/game_bethesda/src/games/fallout4london/fo4london.qrc5
-rw-r--r--libs/game_bethesda/src/games/fallout4london/fo4londonbsainvalidation.cpp68
-rw-r--r--libs/game_bethesda/src/games/fallout4london/fo4londonbsainvalidation.h33
-rw-r--r--libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.cpp48
-rw-r--r--libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.h27
-rw-r--r--libs/game_bethesda/src/games/fallout4london/fo4londonmoddatachecker.h29
-rw-r--r--libs/game_bethesda/src/games/fallout4london/fo4londonmoddatacontent.h44
-rw-r--r--libs/game_bethesda/src/games/fallout4london/fo4londonsavegame.cpp108
-rw-r--r--libs/game_bethesda/src/games/fallout4london/fo4londonsavegame.h23
-rw-r--r--libs/game_bethesda/src/games/fallout4london/fo4londonscriptextender.cpp18
-rw-r--r--libs/game_bethesda/src/games/fallout4london/fo4londonscriptextender.h17
-rw-r--r--libs/game_bethesda/src/games/fallout4london/fo4londonunmanagedmods.cpp63
-rw-r--r--libs/game_bethesda/src/games/fallout4london/fo4londonunmanagedmods.h18
-rw-r--r--libs/game_bethesda/src/games/fallout4london/game_fo4london_en.ts27
-rw-r--r--libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp366
-rw-r--r--libs/game_bethesda/src/games/fallout4london/gamefo4london.h78
-rw-r--r--libs/game_bethesda/src/games/fallout4london/gamefo4london.json1
-rw-r--r--libs/game_bethesda/src/games/fallout4london/splash.pngbin0 -> 52981 bytes
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/CMakeLists.txt22
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/fallout4vr.qrc5
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.cpp49
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.h27
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/fallout4vrgameplugins.cpp15
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/fallout4vrgameplugins.h19
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/fallout4vrmoddatachecker.h30
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/fallout4vrmoddatacontent.h44
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/fallout4vrsavegame.cpp108
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/fallout4vrsavegame.h23
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/fallout4vrscriptextender.cpp18
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/fallout4vrscriptextender.h17
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/fallout4vrunmanagedmods.cpp63
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/fallout4vrunmanagedmods.h18
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/game_fallout4vr_en.ts18
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp251
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.h59
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.json1
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/splash.pngbin0 -> 52981 bytes
-rw-r--r--libs/game_bethesda/src/games/fallout76/CMakeLists.txt22
-rw-r--r--libs/game_bethesda/src/games/fallout76/fallout76.qrc5
-rw-r--r--libs/game_bethesda/src/games/fallout76/fallout76dataarchives.cpp150
-rw-r--r--libs/game_bethesda/src/games/fallout76/fallout76dataarchives.h33
-rw-r--r--libs/game_bethesda/src/games/fallout76/fallout76moddatachecker.h44
-rw-r--r--libs/game_bethesda/src/games/fallout76/fallout76moddatacontent.h44
-rw-r--r--libs/game_bethesda/src/games/fallout76/fallout76savegame.cpp106
-rw-r--r--libs/game_bethesda/src/games/fallout76/fallout76savegame.h23
-rw-r--r--libs/game_bethesda/src/games/fallout76/fallout76savegameinfo.cpp10
-rw-r--r--libs/game_bethesda/src/games/fallout76/fallout76savegameinfo.h15
-rw-r--r--libs/game_bethesda/src/games/fallout76/fallout76scriptextender.cpp18
-rw-r--r--libs/game_bethesda/src/games/fallout76/fallout76scriptextender.h17
-rw-r--r--libs/game_bethesda/src/games/fallout76/fallout76unmanagedmods.cpp46
-rw-r--r--libs/game_bethesda/src/games/fallout76/fallout76unmanagedmods.h18
-rw-r--r--libs/game_bethesda/src/games/fallout76/game_fallout76_en.ts13
-rw-r--r--libs/game_bethesda/src/games/fallout76/gamefallout76.cpp227
-rw-r--r--libs/game_bethesda/src/games/fallout76/gamefallout76.h55
-rw-r--r--libs/game_bethesda/src/games/fallout76/gamefallout76.json1
-rw-r--r--libs/game_bethesda/src/games/fallout76/splash.pngbin0 -> 52981 bytes
-rw-r--r--libs/game_bethesda/src/games/falloutnv/CMakeLists.txt31
-rw-r--r--libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.cpp16
-rw-r--r--libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.h20
-rw-r--r--libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.cpp32
-rw-r--r--libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.h23
-rw-r--r--libs/game_bethesda/src/games/falloutnv/falloutnvmoddatachecker.h51
-rw-r--r--libs/game_bethesda/src/games/falloutnv/falloutnvmoddatacontent.h22
-rw-r--r--libs/game_bethesda/src/games/falloutnv/falloutnvsavegame.cpp77
-rw-r--r--libs/game_bethesda/src/games/falloutnv/falloutnvsavegame.h23
-rw-r--r--libs/game_bethesda/src/games/falloutnv/falloutnvscriptextender.cpp18
-rw-r--r--libs/game_bethesda/src/games/falloutnv/falloutnvscriptextender.h17
-rw-r--r--libs/game_bethesda/src/games/falloutnv/game_falloutNV_en.ts22
-rw-r--r--libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp334
-rw-r--r--libs/game_bethesda/src/games/falloutnv/gamefalloutnv.h70
-rw-r--r--libs/game_bethesda/src/games/falloutnv/gamefalloutnv.json1
-rw-r--r--libs/game_bethesda/src/games/morrowind/CMakeLists.txt27
-rw-r--r--libs/game_bethesda/src/games/morrowind/game_morrowind_en.ts69
-rw-r--r--libs/game_bethesda/src/games/morrowind/gamemorrowind.cpp216
-rw-r--r--libs/game_bethesda/src/games/morrowind/gamemorrowind.h62
-rw-r--r--libs/game_bethesda/src/games/morrowind/gamemorrowind.json1
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowind.qrc5
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.cpp16
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.h20
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowinddataarchives.cpp71
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowinddataarchives.h28
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindgameplugins.cpp197
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindgameplugins.h28
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.cpp56
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.h41
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindmoddatachecker.h26
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindmoddatacontent.h24
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindsavegame.cpp183
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindsavegame.h57
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.cpp18
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.h22
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.cpp123
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.h31
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.ui248
-rw-r--r--libs/game_bethesda/src/games/morrowind/splash.pngbin0 -> 83336 bytes
-rw-r--r--libs/game_bethesda/src/games/nehrim/CMakeLists.txt21
-rw-r--r--libs/game_bethesda/src/games/nehrim/game_nehrim_en.ts17
-rw-r--r--libs/game_bethesda/src/games/nehrim/gamenehrim.cpp205
-rw-r--r--libs/game_bethesda/src/games/nehrim/gamenehrim.h56
-rw-r--r--libs/game_bethesda/src/games/nehrim/gamenehrim.json1
-rw-r--r--libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.cpp16
-rw-r--r--libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.h20
-rw-r--r--libs/game_bethesda/src/games/nehrim/nehrimdataarchives.cpp31
-rw-r--r--libs/game_bethesda/src/games/nehrim/nehrimdataarchives.h23
-rw-r--r--libs/game_bethesda/src/games/nehrim/nehrimmoddatachecker.cpp30
-rw-r--r--libs/game_bethesda/src/games/nehrim/nehrimmoddatachecker.h35
-rw-r--r--libs/game_bethesda/src/games/nehrim/nehrimmoddatacontent.h22
-rw-r--r--libs/game_bethesda/src/games/nehrim/nehrimsavegame.cpp72
-rw-r--r--libs/game_bethesda/src/games/nehrim/nehrimsavegame.h21
-rw-r--r--libs/game_bethesda/src/games/nehrim/nehrimscriptextender.cpp20
-rw-r--r--libs/game_bethesda/src/games/nehrim/nehrimscriptextender.h18
-rw-r--r--libs/game_bethesda/src/games/oblivion/CMakeLists.txt21
-rw-r--r--libs/game_bethesda/src/games/oblivion/game_oblivion_en.ts17
-rw-r--r--libs/game_bethesda/src/games/oblivion/gameoblivion.cpp213
-rw-r--r--libs/game_bethesda/src/games/oblivion/gameoblivion.h51
-rw-r--r--libs/game_bethesda/src/games/oblivion/gameoblivion.json1
-rw-r--r--libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.cpp16
-rw-r--r--libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.h20
-rw-r--r--libs/game_bethesda/src/games/oblivion/obliviondataarchives.cpp32
-rw-r--r--libs/game_bethesda/src/games/oblivion/obliviondataarchives.h23
-rw-r--r--libs/game_bethesda/src/games/oblivion/oblivionmoddatachecker.cpp30
-rw-r--r--libs/game_bethesda/src/games/oblivion/oblivionmoddatachecker.h35
-rw-r--r--libs/game_bethesda/src/games/oblivion/oblivionmoddatacontent.h22
-rw-r--r--libs/game_bethesda/src/games/oblivion/oblivionsavegame.cpp74
-rw-r--r--libs/game_bethesda/src/games/oblivion/oblivionsavegame.h21
-rw-r--r--libs/game_bethesda/src/games/oblivion/oblivionscriptextender.cpp20
-rw-r--r--libs/game_bethesda/src/games/oblivion/oblivionscriptextender.h18
-rw-r--r--libs/game_bethesda/src/games/skyrim/CMakeLists.txt22
-rw-r--r--libs/game_bethesda/src/games/skyrim/game_skyrim_en.ts17
-rw-r--r--libs/game_bethesda/src/games/skyrim/gameskyrim.cpp263
-rw-r--r--libs/game_bethesda/src/games/skyrim/gameskyrim.h55
-rw-r--r--libs/game_bethesda/src/games/skyrim/gameskyrim.json1
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp16
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.h20
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp42
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimdataarchives.h23
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimgameplugins.cpp123
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimgameplugins.h23
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimmoddatachecker.h50
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimmoddatacontent.h14
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimsavegame.cpp105
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimsavegame.h28
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimscriptextender.cpp18
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimscriptextender.h17
-rw-r--r--libs/game_bethesda/src/games/skyrimse/CMakeLists.txt36
-rw-r--r--libs/game_bethesda/src/games/skyrimse/game_skyrimse_en.ts17
-rw-r--r--libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp359
-rw-r--r--libs/game_bethesda/src/games/skyrimse/gameskyrimse.h72
-rw-r--r--libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.cpp44
-rw-r--r--libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.h25
-rw-r--r--libs/game_bethesda/src/games/skyrimse/skyrimsemoddatachecker.h42
-rw-r--r--libs/game_bethesda/src/games/skyrimse/skyrimsemoddatacontent.h21
-rw-r--r--libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.cpp157
-rw-r--r--libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.h28
-rw-r--r--libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.cpp18
-rw-r--r--libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.h17
-rw-r--r--libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.cpp29
-rw-r--r--libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.h16
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/CMakeLists.txt22
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/game_skyrimvr_en.ts17
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.cpp306
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.h70
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.json1
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.cpp45
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.h26
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/skyrimvrgameplugins.cpp15
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/skyrimvrgameplugins.h19
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/skyrimvrmoddatachecker.h31
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/skyrimvrmoddatacontent.h21
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/skyrimvrsavegame.cpp149
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/skyrimvrsavegame.h24
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/skyrimvrscriptextender.cpp18
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/skyrimvrscriptextender.h17
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/skyrimvrunmanagedmods.cpp29
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/skyrimvrunmanagedmods.h16
-rw-r--r--libs/game_bethesda/src/games/starfield/CMakeLists.txt24
-rw-r--r--libs/game_bethesda/src/games/starfield/game_starfield_en.ts73
-rw-r--r--libs/game_bethesda/src/games/starfield/gamestarfield.cpp438
-rw-r--r--libs/game_bethesda/src/games/starfield/gamestarfield.h83
-rw-r--r--libs/game_bethesda/src/games/starfield/gamestarfield.json1
-rw-r--r--libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.cpp68
-rw-r--r--libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.h33
-rw-r--r--libs/game_bethesda/src/games/starfield/starfielddataarchives.cpp98
-rw-r--r--libs/game_bethesda/src/games/starfield/starfielddataarchives.h28
-rw-r--r--libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp35
-rw-r--r--libs/game_bethesda/src/games/starfield/starfieldgameplugins.h23
-rw-r--r--libs/game_bethesda/src/games/starfield/starfieldmoddatachecker.h29
-rw-r--r--libs/game_bethesda/src/games/starfield/starfieldmoddatacontent.h55
-rw-r--r--libs/game_bethesda/src/games/starfield/starfieldsavegame.cpp157
-rw-r--r--libs/game_bethesda/src/games/starfield/starfieldsavegame.h27
-rw-r--r--libs/game_bethesda/src/games/starfield/starfieldscriptextender.cpp18
-rw-r--r--libs/game_bethesda/src/games/starfield/starfieldscriptextender.h17
-rw-r--r--libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp132
-rw-r--r--libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.h36
-rw-r--r--libs/game_bethesda/src/games/ttw/CMakeLists.txt20
-rw-r--r--libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp16
-rw-r--r--libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.h20
-rw-r--r--libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp31
-rw-r--r--libs/game_bethesda/src/games/ttw/falloutttwdataarchives.h23
-rw-r--r--libs/game_bethesda/src/games/ttw/falloutttwmoddatachecker.h51
-rw-r--r--libs/game_bethesda/src/games/ttw/falloutttwmoddatacontent.h22
-rw-r--r--libs/game_bethesda/src/games/ttw/falloutttwsavegame.cpp79
-rw-r--r--libs/game_bethesda/src/games/ttw/falloutttwsavegame.h23
-rw-r--r--libs/game_bethesda/src/games/ttw/falloutttwscriptextender.cpp18
-rw-r--r--libs/game_bethesda/src/games/ttw/falloutttwscriptextender.h17
-rw-r--r--libs/game_bethesda/src/games/ttw/game_ttw_en.ts22
-rw-r--r--libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp364
-rw-r--r--libs/game_bethesda/src/games/ttw/gamefalloutttw.h77
-rw-r--r--libs/game_bethesda/src/games/ttw/gamefalloutttw.json1
-rw-r--r--libs/game_bethesda/vcpkg.json31
326 files changed, 19809 insertions, 0 deletions
diff --git a/libs/game_bethesda/.clang-format b/libs/game_bethesda/.clang-format
new file mode 100644
index 0000000..6098e1f
--- /dev/null
+++ b/libs/game_bethesda/.clang-format
@@ -0,0 +1,41 @@
+---
+# We'll use defaults from the LLVM style, but with 4 columns indentation.
+BasedOnStyle: LLVM
+IndentWidth: 2
+---
+Language: Cpp
+DeriveLineEnding: false
+UseCRLF: true
+DerivePointerAlignment: false
+PointerAlignment: Left
+AlignConsecutiveAssignments: true
+AllowShortFunctionsOnASingleLine: Inline
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLambdasOnASingleLine: Empty
+AlwaysBreakTemplateDeclarations: Yes
+AccessModifierOffset: -2
+AlignTrailingComments: true
+SpacesBeforeTrailingComments: 2
+NamespaceIndentation: Inner
+MaxEmptyLinesToKeep: 1
+BreakBeforeBraces: Custom
+BraceWrapping:
+ AfterCaseLabel: false
+ AfterClass: true
+ AfterControlStatement: false
+ AfterEnum: true
+ AfterFunction: true
+ AfterNamespace: true
+ AfterStruct: true
+ AfterUnion: true
+ AfterExternBlock: true
+ BeforeCatch: false
+ BeforeElse: false
+ BeforeLambdaBody: false
+ BeforeWhile: false
+ IndentBraces: false
+ SplitEmptyFunction: false
+ SplitEmptyRecord: false
+ SplitEmptyNamespace: true
+ColumnLimit: 88
+ForEachMacros: ['Q_FOREACH', 'foreach']
diff --git a/libs/game_bethesda/.git-blame-ignore-revs b/libs/game_bethesda/.git-blame-ignore-revs
new file mode 100644
index 0000000..6b3e971
--- /dev/null
+++ b/libs/game_bethesda/.git-blame-ignore-revs
@@ -0,0 +1,15 @@
+484afb68695eadfa57f1cf9ffc9b6e166ee20174
+59cc8639ca60de146e19de64f9dbf4380f52bcf3
+dba50ca6cecfd73fc917bf19325947fdc06ab2c3
+a070d542b2185c9568e309e540b8e61178b88ad4
+99cc4566477a37dc07eeb6282ee5e4307a6d1914
+2414eb383ad508992ab17d958f3959dc1a566748
+7cce6c37f23c886f388e98828d496edd6c5d72f6
+d8d13d2f4b54eaa4409ae97c168e0e4600c743c6
+8cd4ed42e757921dcf18e6bf0a9d82fbc586a95a
+2055815ed96457fc559b3ab2323403f3b20cbe00
+168c64df2fb816d78e6cda9cec12ac4749ab4497
+64d324bb78c54ed9d6cecb8e21aceb626ee8f36a
+c4d7eaab7f15f8b1954cfeb56c3314f9b0e9c7ec
+632ce9843dc9c263203ce4f376ebcfb1672cf1c3
+891a7103e31bf928441ef8ae5a45cd1b21f1abb0
diff --git a/libs/game_bethesda/.gitattributes b/libs/game_bethesda/.gitattributes
new file mode 100644
index 0000000..f869712
--- /dev/null
+++ b/libs/game_bethesda/.gitattributes
@@ -0,0 +1,7 @@
+# Set the default behavior, in case people don't have core.autocrlf set.
+* text=auto
+
+# Explicitly declare text files you want to always be normalized and converted
+# to native line endings on checkout.
+*.cpp text eol=crlf
+*.h text eol=crlf
diff --git a/libs/game_bethesda/.github/workflows/build.yml b/libs/game_bethesda/.github/workflows/build.yml
new file mode 100644
index 0000000..a9c5b0e
--- /dev/null
+++ b/libs/game_bethesda/.github/workflows/build.yml
@@ -0,0 +1,20 @@
+name: Build Game Bethesda Library
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ types: [opened, synchronize, reopened]
+
+env:
+ VCPKG_BINARY_SOURCES: ${{ vars.AZ_BLOB_VCPKG_URL != '' && format('clear;x-azblob,{0},{1},readwrite', vars.AZ_BLOB_VCPKG_URL, secrets.AZ_BLOB_SAS) || '' }}
+
+jobs:
+ build:
+ runs-on: windows-2022
+ steps:
+ - name: Build Game Bethesda
+ id: build-game-bethesda
+ uses: ModOrganizer2/build-with-mob-action@master
+ with:
+ mo2-dependencies: uibase
diff --git a/libs/game_bethesda/.github/workflows/linting.yml b/libs/game_bethesda/.github/workflows/linting.yml
new file mode 100644
index 0000000..5a2ca91
--- /dev/null
+++ b/libs/game_bethesda/.github/workflows/linting.yml
@@ -0,0 +1,16 @@
+name: Lint Game Bethesda
+
+on:
+ push:
+ pull_request:
+ types: [opened, synchronize, reopened]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Check format
+ uses: ModOrganizer2/check-formatting-action@master
+ with:
+ check-path: "."
diff --git a/libs/game_bethesda/.gitignore b/libs/game_bethesda/.gitignore
new file mode 100644
index 0000000..cf71be7
--- /dev/null
+++ b/libs/game_bethesda/.gitignore
@@ -0,0 +1,5 @@
+edit
+CMakeLists.txt.user
+/msbuild.log
+/*std*.log
+/*build
diff --git a/libs/game_bethesda/.pre-commit-config.yaml b/libs/game_bethesda/.pre-commit-config.yaml
new file mode 100644
index 0000000..eb8969c
--- /dev/null
+++ b/libs/game_bethesda/.pre-commit-config.yaml
@@ -0,0 +1,20 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v6.0.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-merge-conflict
+ - id: check-case-conflict
+ - repo: https://github.com/pre-commit/mirrors-clang-format
+ rev: v21.1.8
+ hooks:
+ - id: clang-format
+ 'types_or': [c++, c]
+
+ci:
+ autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks."
+ autofix_prs: true
+ autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate."
+ autoupdate_schedule: quarterly
+ submodules: false
diff --git a/libs/game_bethesda/CMakeLists.txt b/libs/game_bethesda/CMakeLists.txt
new file mode 100644
index 0000000..40736b5
--- /dev/null
+++ b/libs/game_bethesda/CMakeLists.txt
@@ -0,0 +1,56 @@
+cmake_minimum_required(VERSION 3.16)
+project(game_bethesda)
+
+find_package(Qt6 COMPONENTS Widgets REQUIRED)
+
+# Compatibility helpers for upstream plugin CMake files that still call mo2-cmake
+# functions. We implement only what this Linux port needs.
+function(mo2_configure_plugin target)
+ set_target_properties(${target} PROPERTIES
+ AUTOMOC ON
+ AUTOUIC ON
+ AUTORCC ON
+ CXX_STANDARD 23
+ CXX_STANDARD_REQUIRED ON
+ POSITION_INDEPENDENT_CODE ON
+ )
+
+ # Keep bare uibase headers after system headers to avoid strings.h collisions.
+ target_compile_options(${target} PRIVATE
+ "-idirafter" "${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase"
+ )
+ set_property(TARGET ${target} APPEND PROPERTY AUTOMOC_MOC_OPTIONS
+ "-I${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase"
+ )
+endfunction()
+
+function(mo2_default_source_group)
+ # no-op for now
+endfunction()
+
+function(mo2_install_plugin target)
+ install(TARGETS ${target}
+ LIBRARY DESTINATION plugins
+ )
+endfunction()
+
+add_subdirectory(src/gamebryo)
+add_subdirectory(src/creation)
+
+# Build all ported game plugins.
+add_subdirectory(src/games/skyrimse)
+add_subdirectory(src/games/skyrim)
+add_subdirectory(src/games/skyrimvr)
+add_subdirectory(src/games/fallout4)
+add_subdirectory(src/games/fallout4vr)
+add_subdirectory(src/games/fallout3)
+add_subdirectory(src/games/falloutnv)
+add_subdirectory(src/games/fallout76)
+add_subdirectory(src/games/oblivion)
+add_subdirectory(src/games/morrowind)
+add_subdirectory(src/games/nehrim)
+add_subdirectory(src/games/enderal)
+add_subdirectory(src/games/enderalse)
+add_subdirectory(src/games/starfield)
+add_subdirectory(src/games/ttw)
+add_subdirectory(src/games/fallout4london)
diff --git a/libs/game_bethesda/CMakePresets.json b/libs/game_bethesda/CMakePresets.json
new file mode 100644
index 0000000..cc009b3
--- /dev/null
+++ b/libs/game_bethesda/CMakePresets.json
@@ -0,0 +1,57 @@
+{
+ "configurePresets": [
+ {
+ "errors": {
+ "deprecated": true
+ },
+ "hidden": true,
+ "name": "cmake-dev",
+ "warnings": {
+ "deprecated": true,
+ "dev": true
+ }
+ },
+ {
+ "cacheVariables": {
+ "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": {
+ "type": "BOOL",
+ "value": "ON"
+ }
+ },
+ "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
+ "hidden": true,
+ "name": "vcpkg"
+ },
+ {
+ "hidden": true,
+ "inherits": ["vcpkg"],
+ "name": "vcpkg-dev"
+ },
+ {
+ "binaryDir": "${sourceDir}/vsbuild",
+ "architecture": {
+ "strategy": "set",
+ "value": "x64"
+ },
+ "cacheVariables": {
+ "CMAKE_CXX_FLAGS": "/EHsc /MP /W4",
+ "VCPKG_TARGET_TRIPLET": {
+ "type": "STRING",
+ "value": "x64-windows-static-md"
+ }
+ },
+ "generator": "Visual Studio 17 2022",
+ "inherits": ["cmake-dev", "vcpkg-dev"],
+ "name": "vs2022-windows",
+ "toolset": "v143"
+ }
+ ],
+ "buildPresets": [
+ {
+ "name": "vs2022-windows",
+ "resolvePackageReferences": "on",
+ "configurePreset": "vs2022-windows"
+ }
+ ],
+ "version": 4
+}
diff --git a/libs/game_bethesda/src/creation/CMakeLists.txt b/libs/game_bethesda/src/creation/CMakeLists.txt
new file mode 100644
index 0000000..9aa23d8
--- /dev/null
+++ b/libs/game_bethesda/src/creation/CMakeLists.txt
@@ -0,0 +1,33 @@
+cmake_minimum_required(VERSION 3.16)
+
+find_package(Qt6 COMPONENTS Widgets REQUIRED)
+
+# Try cmake config first, fall back to pkg-config
+find_package(lz4 CONFIG QUIET)
+if(NOT lz4_FOUND)
+ find_package(PkgConfig REQUIRED)
+ pkg_check_modules(LZ4 REQUIRED IMPORTED_TARGET liblz4)
+endif()
+# lz4 1.10+ exports LZ4::lz4, older versions export lz4::lz4
+if(TARGET LZ4::lz4 AND NOT TARGET lz4::lz4)
+ add_library(lz4::lz4 ALIAS LZ4::lz4)
+endif()
+
+set(CMAKE_AUTOMOC ON)
+
+add_library(game_creation STATIC
+ creationgameplugins.h
+ creationgameplugins.cpp
+)
+
+target_link_libraries(game_creation
+ PUBLIC game_gamebryo
+ PRIVATE $<IF:$<TARGET_EXISTS:lz4::lz4>,lz4::lz4,PkgConfig::LZ4>
+)
+
+target_include_directories(game_creation PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+
+# Keep uibase bare includes after system headers to avoid strings.h collision.
+target_compile_options(game_creation PRIVATE
+ "-idirafter" "${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase"
+)
diff --git a/libs/game_bethesda/src/creation/creationgameplugins.cpp b/libs/game_bethesda/src/creation/creationgameplugins.cpp
new file mode 100644
index 0000000..f5cef62
--- /dev/null
+++ b/libs/game_bethesda/src/creation/creationgameplugins.cpp
@@ -0,0 +1,183 @@
+#include "creationgameplugins.h"
+#include <ipluginlist.h>
+#include <report.h>
+#include <safewritefile.h>
+#include <scopeguard.h>
+
+#include <QDir>
+#include <QSet>
+#include <QStringEncoder>
+#include <QStringList>
+
+using MOBase::IOrganizer;
+using MOBase::IPluginGame;
+using MOBase::IPluginList;
+using MOBase::reportError;
+using MOBase::SafeWriteFile;
+
+CreationGamePlugins::CreationGamePlugins(IOrganizer* organizer)
+ : GamebryoGamePlugins(organizer)
+{}
+
+QStringList CreationGamePlugins::getLoadOrder()
+{
+ 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 (loadOrderIsNew || !pluginsIsNew) {
+ return readLoadOrderList(m_Organizer->pluginList(), loadOrderPath);
+ } else {
+ return readPluginList(m_Organizer->pluginList());
+ }
+}
+
+void CreationGamePlugins::writePluginList(const 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<QString> ManagedMods =
+ QSet<QString>(PrimaryPlugins.begin(), PrimaryPlugins.end());
+ QSet<QString> DLCSet = QSet<QString>(DLCPlugins.begin(), DLCPlugins.end());
+ ManagedMods.subtract(DLCSet);
+ PrimaryPlugins.append(QList<QString>(ManagedMods.begin(), ManagedMods.end()));
+
+ // 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();
+}
+
+QStringList CreationGamePlugins::readPluginList(MOBase::IPluginList* pluginList)
+{
+ const auto plugins = pluginList->pluginNames();
+ const auto primaryPlugins = organizer()->managedGame()->primaryPlugins();
+ QStringList loadOrder(primaryPlugins);
+
+ for (const QString& pluginName : loadOrder) {
+ if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) {
+ pluginList->setState(pluginName, IPluginList::STATE_ACTIVE);
+ }
+ }
+
+ QString filePath = organizer()->profile()->absolutePath() + "/plugins.txt";
+ QFile file(filePath);
+ if (!file.open(QIODevice::ReadOnly)) {
+ qWarning("%s not found", qUtf8Printable(filePath));
+ return loadOrder;
+ }
+ ON_BLOCK_EXIT([&]() {
+ file.close();
+ });
+
+ if (file.size() == 0) {
+ // MO stores at least a header in the file. if it's completely empty the
+ // file is broken
+ qWarning("%s empty", qUtf8Printable(filePath));
+ return loadOrder;
+ }
+
+ QStringList pluginsFound;
+ 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 (!primaryPlugins.contains(pluginName, Qt::CaseInsensitive)) {
+ if (pluginName.startsWith('*')) {
+ pluginName.remove(0, 1);
+ if (pluginName.size() > 0) {
+ pluginList->setState(pluginName, IPluginList::STATE_ACTIVE);
+ pluginsFound.append(pluginName);
+ if (!loadOrder.contains(pluginName, Qt::CaseInsensitive)) {
+ loadOrder.append(pluginName);
+ }
+ }
+ } else {
+ if (pluginName.size() > 0) {
+ pluginList->setState(pluginName, IPluginList::STATE_INACTIVE);
+ pluginsFound.append(pluginName);
+ if (!loadOrder.contains(pluginName, Qt::CaseInsensitive)) {
+ loadOrder.append(pluginName);
+ }
+ }
+ }
+ } else {
+ pluginName.remove(0, 1);
+ pluginsFound.append(pluginName);
+ }
+ }
+
+ file.close();
+
+ // set all plugins not found inactive
+ for (const auto& pluginName : plugins) {
+ if (!pluginsFound.contains(pluginName, Qt::CaseInsensitive)) {
+ pluginList->setState(pluginName, IPluginList::STATE_INACTIVE);
+ }
+ }
+
+ return loadOrder;
+}
+
+bool CreationGamePlugins::lightPluginsAreSupported()
+{
+ return true;
+}
diff --git a/libs/game_bethesda/src/creation/creationgameplugins.h b/libs/game_bethesda/src/creation/creationgameplugins.h
new file mode 100644
index 0000000..f7afd80
--- /dev/null
+++ b/libs/game_bethesda/src/creation/creationgameplugins.h
@@ -0,0 +1,22 @@
+#ifndef CREATIONGAMEPLUGINS_H
+#define CREATIONGAMEPLUGINS_H
+
+#include <gamebryogameplugins.h>
+#include <imoinfo.h>
+#include <iplugingame.h>
+#include <map>
+
+class CreationGamePlugins : public GamebryoGamePlugins
+{
+public:
+ CreationGamePlugins(MOBase::IOrganizer* organizer);
+
+protected:
+ virtual void writePluginList(const MOBase::IPluginList* pluginList,
+ const QString& filePath) override;
+ virtual QStringList readPluginList(MOBase::IPluginList* pluginList) override;
+ virtual QStringList getLoadOrder() override;
+ virtual bool lightPluginsAreSupported() override;
+};
+
+#endif // CREATIONGAMEPLUGINS_H
diff --git a/libs/game_bethesda/src/creation/game_creation_en.ts b/libs/game_bethesda/src/creation/game_creation_en.ts
new file mode 100644
index 0000000..f217448
--- /dev/null
+++ b/libs/game_bethesda/src/creation/game_creation_en.ts
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>QObject</name>
+ <message>
+ <location filename="creationgameplugins.cpp" line="97"/>
+ <source>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.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/libs/game_bethesda/src/gamebryo/CMakeLists.txt b/libs/game_bethesda/src/gamebryo/CMakeLists.txt
new file mode 100644
index 0000000..bbc47ae
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/CMakeLists.txt
@@ -0,0 +1,78 @@
+cmake_minimum_required(VERSION 3.16)
+
+find_package(Qt6 COMPONENTS Widgets REQUIRED)
+find_package(ZLIB REQUIRED)
+
+# Try cmake config first, fall back to pkg-config
+find_package(lz4 CONFIG QUIET)
+if(NOT lz4_FOUND)
+ find_package(PkgConfig REQUIRED)
+ pkg_check_modules(LZ4 REQUIRED IMPORTED_TARGET liblz4)
+endif()
+# lz4 1.10+ exports LZ4::lz4, older versions export lz4::lz4
+if(TARGET LZ4::lz4 AND NOT TARGET lz4::lz4)
+ add_library(lz4::lz4 ALIAS LZ4::lz4)
+endif()
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTOUIC ON)
+
+add_library(game_gamebryo STATIC
+ dummybsa.cpp
+ dummybsa.h
+ gamebryobsainvalidation.cpp
+ gamebryobsainvalidation.h
+ gamebryodataarchives.cpp
+ gamebryodataarchives.h
+ gamebryogameplugins.cpp
+ gamebryogameplugins.h
+ gamebryolocalsavegames.cpp
+ gamebryolocalsavegames.h
+ gamebryomoddatachecker.cpp
+ gamebryomoddatachecker.h
+ gamebryomoddatacontent.cpp
+ gamebryomoddatacontent.h
+ gamebryosavegame.cpp
+ gamebryosavegame.h
+ gamebryosavegameinfo.cpp
+ gamebryosavegameinfo.h
+ gamebryosavegameinfowidget.cpp
+ gamebryosavegameinfowidget.h
+ gamebryosavegameinfowidget.ui
+ gamebryoscriptextender.cpp
+ gamebryoscriptextender.h
+ gamebryounmanagedmods.cpp
+ gamebryounmanagedmods.h
+ gamegamebryo.cpp
+ gamegamebryo.h
+ vdf_parser.h
+)
+
+target_link_libraries(game_gamebryo
+ PUBLIC
+ uibase
+ game_features
+ Qt6::Widgets
+ PRIVATE
+ ZLIB::ZLIB
+ $<IF:$<TARGET_EXISTS:lz4::lz4>,lz4::lz4,PkgConfig::LZ4>
+)
+
+# NaK FFI for game detection (replaces Windows registry)
+if(TARGET mo2::nak_ffi)
+ target_link_libraries(game_gamebryo PRIVATE mo2::nak_ffi)
+ target_compile_definitions(game_gamebryo PRIVATE HAS_NAK_FFI)
+endif()
+
+target_include_directories(game_gamebryo PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+
+# Keep uibase bare includes after system headers to avoid strings.h collision.
+target_compile_options(game_gamebryo PRIVATE
+ "-idirafter" "${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase"
+)
+
+# AUTOMOC does not reliably honor -idirafter, so provide the bare uibase include
+# path explicitly for moc parsing of Q_INTERFACES declarations.
+set_property(TARGET game_gamebryo APPEND PROPERTY AUTOMOC_MOC_OPTIONS
+ "-I${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase"
+)
diff --git a/libs/game_bethesda/src/gamebryo/dummybsa.cpp b/libs/game_bethesda/src/gamebryo/dummybsa.cpp
new file mode 100644
index 0000000..7b0f877
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/dummybsa.cpp
@@ -0,0 +1,198 @@
+/*
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "dummybsa.h"
+#include <QFile>
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN
+#include <Windows.h>
+#else
+#ifndef MAX_PATH
+#define MAX_PATH 260
+#endif
+#endif
+
+static void writeUlong(unsigned char* buffer, int offset, unsigned long value)
+{
+ union
+ {
+ unsigned long ulValue;
+ unsigned char cValue[4];
+ };
+ ulValue = value;
+ memcpy(buffer + offset, cValue, 4);
+}
+
+static void writeUlonglong(unsigned char* buffer, int offset, unsigned long long value)
+{
+ union
+ {
+ unsigned long long ullValue;
+ unsigned char cValue[8];
+ };
+ ullValue = value;
+ memcpy(buffer + offset, cValue, 8);
+}
+
+static unsigned long genHashInt(const unsigned char* pos, const unsigned char* end)
+{
+ unsigned long hash = 0;
+ for (; pos < end; ++pos) {
+ hash *= 0x1003f;
+ hash += *pos;
+ }
+ return hash;
+}
+
+static unsigned long long genHash(const char* fileName)
+{
+ char fileNameLower[MAX_PATH + 1];
+ int i = 0;
+ for (; i < MAX_PATH && fileName[i] != '\0'; ++i) {
+ fileNameLower[i] = static_cast<char>(tolower(fileName[i]));
+ if (fileNameLower[i] == '\\') {
+ fileNameLower[i] = '/';
+ }
+ }
+ fileNameLower[i] = '\0';
+
+ unsigned char* fileNameLowerU = reinterpret_cast<unsigned char*>(fileNameLower);
+
+ char* ext = strrchr(fileNameLower, '.');
+ if (ext == nullptr) {
+ ext = fileNameLower + strlen(fileNameLower);
+ }
+ unsigned char* extU = reinterpret_cast<unsigned char*>(ext);
+
+ int length = ext - fileNameLower;
+
+ unsigned long long hash = 0ULL;
+
+ if (length > 0) {
+ hash = *(extU - 1) | ((length > 2 ? *(ext - 2) : 0) << 8) | (length << 16) |
+ (fileNameLowerU[0] << 24);
+ }
+
+ if (strlen(ext) > 0) {
+ if (strcmp(ext + 1, "kf") == 0) {
+ hash |= 0x80;
+ } else if (strcmp(ext + 1, "nif") == 0) {
+ hash |= 0x8000;
+ } else if (strcmp(ext + 1, "dds") == 0) {
+ hash |= 0x8080;
+ } else if (strcmp(ext + 1, "wav") == 0) {
+ hash |= 0x80000000;
+ }
+
+ unsigned long long temp =
+ static_cast<unsigned long long>(genHashInt(fileNameLowerU + 1, extU - 2));
+ temp += static_cast<unsigned long long>(genHashInt(extU, extU + strlen(ext)));
+
+ hash |= (temp & 0xFFFFFFFF) << 32;
+ }
+ return hash;
+}
+
+DummyBSA::DummyBSA(unsigned long bsaVersion)
+ : m_Version(bsaVersion), m_FolderName(""), m_FileName("dummy.dds"),
+ m_TotalFileNameLength(0)
+{}
+
+void DummyBSA::writeHeader(QFile& file)
+{
+ unsigned char header[] = {
+ 'B', 'S', 'A', '\0', // magic string
+ 0xDE, 0xAD, 0xBE, 0xEF, // version - insert later
+ 0x24, 0x00, 0x00, 0x00, // offset to folder recors. header size is static
+ 0xDE, 0xAD, 0xBE, 0xEF, // archive flags - insert later
+ 0x01, 0x00, 0x00, 0x00, // folder count
+ 0x01, 0x00, 0x00, 0x00, // file count
+ 0xDE, 0xAD, 0xBE, 0xEF, // total folder names length - insert later
+ 0xDE, 0xAD, 0xBE, 0xEF, // total file names length - insert later
+ 0xDE, 0xAD, 0xBE, 0xEF // file flags - insert later
+ };
+
+ writeUlong(header, 4, m_Version);
+ writeUlong(header, 12, 0x01 | 0x02); // has directories and has files.
+ writeUlong(header, 24,
+ static_cast<unsigned long>(m_FolderName.length()) +
+ 1); // empty folder name
+ writeUlong(header, 28, m_TotalFileNameLength); // single character file name
+
+ writeUlong(header, 32, 2); // has dds
+
+ file.write(reinterpret_cast<char*>(header), sizeof(header));
+}
+
+void DummyBSA::writeFolderRecord(QFile& file, const std::string& folderName)
+{
+ unsigned char folderRecord[] = {
+ 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, // folder hash
+ 0x01, 0x00, 0x00, 0x00, // file count
+ 0xDE, 0xAD, 0xBE, 0xEF, // offset to folder name
+ };
+ // we'd usually have to sort folders be the hash value generated here
+ writeUlonglong(folderRecord, 0, genHash(folderName.c_str()));
+ writeUlong(folderRecord, 12,
+ 0x34 + m_TotalFileNameLength); // TODO: this should be calculated properly
+
+ file.write(reinterpret_cast<char*>(folderRecord), sizeof(folderRecord));
+}
+
+void DummyBSA::writeFileRecord(QFile& file, const std::string& fileName)
+{
+ unsigned char fileRecord[] = {
+ 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, // file name hash
+ 0xDE, 0xAD, 0xBE, 0xEF, // size
+ 0xDE, 0xAD, 0xBE, 0xEF, // offset to file data
+ };
+
+ // we'd usually have to sort files by the value generated here
+ writeUlonglong(fileRecord, 0, genHash(fileName.c_str()));
+ writeUlong(fileRecord, 8, 0);
+ writeUlong(
+ fileRecord, 12,
+ 0x44 + static_cast<unsigned long>(fileName.length() + 1) +
+ 4); // after this record we expect the filename and 4 bytes of file size
+
+ file.write(reinterpret_cast<char*>(fileRecord), sizeof(fileRecord));
+}
+
+void DummyBSA::writeFileRecordBlocks(QFile& file, const std::string& folderName)
+{
+ file.write(folderName.c_str(), folderName.length() + 1);
+
+ writeFileRecord(file, m_FileName);
+}
+
+void DummyBSA::write(const QString& fileName)
+{
+ QFile file(fileName);
+ file.open(QIODevice::WriteOnly);
+
+ m_TotalFileNameLength = static_cast<unsigned long>(m_FileName.length() + 1);
+
+ writeHeader(file);
+ writeFolderRecord(file, m_FolderName);
+ writeFileRecordBlocks(file, m_FolderName);
+ file.write(m_FileName.c_str(), m_FileName.length() + 1);
+ char fileSize[] = {0x00, 0x00, 0x00, 0x00};
+ file.write(fileSize, sizeof(fileSize));
+ file.close();
+}
diff --git a/libs/game_bethesda/src/gamebryo/dummybsa.h b/libs/game_bethesda/src/gamebryo/dummybsa.h
new file mode 100644
index 0000000..8ea071b
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/dummybsa.h
@@ -0,0 +1,59 @@
+/*
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef DUMMYBSA_H
+#define DUMMYBSA_H
+
+#include <QFile>
+#include <QString>
+
+/**
+ * @brief Class for creating a dummy bsa used for archive invalidation
+ **/
+class DummyBSA
+{
+
+public:
+ /**
+ * @brief constructor
+ *
+ **/
+ DummyBSA(unsigned long bsaVersion);
+
+ /**
+ * @brief write to the specified file
+ *
+ * @param fileName name of the file to write to
+ **/
+ void write(const QString& fileName);
+
+private:
+ void writeHeader(QFile& file);
+ void writeFolderRecord(QFile& file, const std::string& folderName);
+ void writeFileRecord(QFile& file, const std::string& fileName);
+ void writeFileRecordBlocks(QFile& file, const std::string& folderName);
+
+private:
+ unsigned long m_Version;
+ std::string m_FolderName;
+ std::string m_FileName;
+ unsigned long m_TotalFileNameLength;
+};
+
+#endif // DUMMYBSA_H
diff --git a/libs/game_bethesda/src/gamebryo/game_gamebryo_en.ts b/libs/game_bethesda/src/gamebryo/game_gamebryo_en.ts
new file mode 100644
index 0000000..add224d
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/game_gamebryo_en.ts
@@ -0,0 +1,171 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GamebryoModDataContent</name>
+ <message>
+ <location filename="gamebryomoddatacontent.cpp" line="15"/>
+ <source>Plugins (ESP/ESM/ESL)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryomoddatacontent.cpp" line="16"/>
+ <source>Optional Plugins</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryomoddatacontent.cpp" line="17"/>
+ <source>Interface</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryomoddatacontent.cpp" line="18"/>
+ <source>Meshes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryomoddatacontent.cpp" line="19"/>
+ <source>Bethesda Archive</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryomoddatacontent.cpp" line="20"/>
+ <source>Scripts (Papyrus)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryomoddatacontent.cpp" line="21"/>
+ <source>Script Extender Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryomoddatacontent.cpp" line="22"/>
+ <source>Script Extender Files</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryomoddatacontent.cpp" line="23"/>
+ <source>SkyProc Patcher</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryomoddatacontent.cpp" line="24"/>
+ <source>Sound or Music</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryomoddatacontent.cpp" line="25"/>
+ <source>Textures</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryomoddatacontent.cpp" line="26"/>
+ <source>MCM Configuration</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryomoddatacontent.cpp" line="27"/>
+ <source>INI Files</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryomoddatacontent.cpp" line="28"/>
+ <source>FaceGen Data</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryomoddatacontent.cpp" line="29"/>
+ <source>ModGroup Files</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>GamebryoSaveGameInfoWidget</name>
+ <message>
+ <location filename="gamebryosavegameinfowidget.ui" line="39"/>
+ <source>Save #</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryosavegameinfowidget.ui" line="51"/>
+ <source>Character</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryosavegameinfowidget.ui" line="63"/>
+ <source>Level</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryosavegameinfowidget.ui" line="75"/>
+ <source>Location</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryosavegameinfowidget.ui" line="87"/>
+ <source>Date</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryosavegameinfowidget.cpp" line="78"/>
+ <source>Has Script Extender Data</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryosavegameinfowidget.cpp" line="83"/>
+ <source>Missing ESPs</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryosavegameinfowidget.cpp" line="116"/>
+ <location filename="gamebryosavegameinfowidget.cpp" line="154"/>
+ <location filename="gamebryosavegameinfowidget.cpp" line="193"/>
+ <source>None</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryosavegameinfowidget.cpp" line="122"/>
+ <source>Missing ESHs</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryosavegameinfowidget.cpp" line="161"/>
+ <source>Missing ESLs</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>QObject</name>
+ <message>
+ <location filename="gamebryogameplugins.cpp" line="131"/>
+ <location filename="../creation/creationgameplugins.cpp" line="97"/>
+ <source>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.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryosavegame.cpp" line="48"/>
+ <source>%1, #%2, Level %3, %4</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryosavegame.cpp" line="102"/>
+ <source>failed to open %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamebryosavegame.cpp" line="112"/>
+ <source>wrong file format - expected %1 got &apos;%2&apos; for %3</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamegamebryo.cpp" line="319"/>
+ <source>failed to query registry path (preflight): %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamegamebryo.cpp" line="327"/>
+ <source>failed to query registry path (read): %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.cpp b/libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.cpp
new file mode 100644
index 0000000..6170305
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.cpp
@@ -0,0 +1,165 @@
+#include "gamebryobsainvalidation.h"
+
+#include "dummybsa.h"
+#include "iplugingame.h"
+#include "iprofile.h"
+#include "registry.h"
+#include <imoinfo.h>
+#include <utility.h>
+
+#include <QDir>
+#include <QFileInfo>
+#include <QStringList>
+
+#include "gamegamebryo.h"
+
+GamebryoBSAInvalidation::GamebryoBSAInvalidation(MOBase::DataArchives* dataArchives,
+ const QString& iniFilename,
+ MOBase::IPluginGame const* game)
+ : m_DataArchives(dataArchives), m_IniFileName(iniFilename), m_Game(game)
+{}
+
+bool GamebryoBSAInvalidation::isInvalidationBSA(const QString& bsaName)
+{
+ static QStringList invalidation{invalidationBSAName()};
+
+ for (const QString& file : invalidation) {
+ if (file.compare(bsaName, Qt::CaseInsensitive) == 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void GamebryoBSAInvalidation::deactivate(MOBase::IProfile* profile)
+{
+ prepareProfile(profile);
+}
+
+void GamebryoBSAInvalidation::activate(MOBase::IProfile* profile)
+{
+ prepareProfile(profile);
+}
+
+// Case-insensitive file resolution (same logic as gamegamebryo.cpp).
+static QString resolveIniPathCaseInsensitive(const QString& filePath)
+{
+#ifdef _WIN32
+ return filePath;
+#else
+ if (QFileInfo(filePath).exists()) {
+ return filePath;
+ }
+ QFileInfo info(filePath);
+ QDir dir(info.path());
+ if (!dir.exists()) {
+ return filePath;
+ }
+ const QString target = info.fileName();
+ const QStringList entries = dir.entryList(QDir::Files | QDir::Hidden);
+ for (const QString& entry : entries) {
+ if (entry.compare(target, Qt::CaseInsensitive) == 0) {
+ return dir.absoluteFilePath(entry);
+ }
+ }
+ return filePath;
+#endif
+}
+
+bool GamebryoBSAInvalidation::prepareProfile(MOBase::IProfile* profile)
+{
+ bool dirty = false;
+ QString basePath = profile->localSettingsEnabled()
+ ? profile->absolutePath()
+ : m_Game->documentsDirectory().absolutePath();
+
+ // Ensure the target INI has adequate content before writing to it.
+ // On Linux, the game launcher may not work, leaving INI files missing
+ // or as empty stubs. Seed from the game's default INI if needed.
+ const auto* gamebryo = dynamic_cast<const GameGamebryo*>(m_Game);
+ if (gamebryo) {
+ const_cast<GameGamebryo*>(gamebryo)->ensureIniFilesExist(basePath);
+ }
+
+ // Resolve case-insensitively (e.g., fallout.ini vs Fallout.ini on Linux)
+ QString iniFilePath = resolveIniPathCaseInsensitive(basePath + "/" + m_IniFileName);
+
+ // write bInvalidateOlderFiles = 1, if needed
+ QString setting =
+ GameGamebryo::readIniValue(iniFilePath, "Archive", "bInvalidateOlderFiles", "0");
+ if (setting.toLong() != 1) {
+ dirty = true;
+ if (!MOBase::WriteRegistryValue("Archive", "bInvalidateOlderFiles", "1",
+ iniFilePath)) {
+ qWarning("failed to activate BSA invalidation in \"%s\"",
+ qUtf8Printable(m_IniFileName));
+ }
+ }
+
+ if (profile->invalidationActive(nullptr)) {
+
+ // add the dummy bsa to the archive string, if needed
+ QStringList archives = m_DataArchives->archives(profile);
+ bool bsaInstalled = false;
+ for (const QString& archive : archives) {
+ if (isInvalidationBSA(archive)) {
+ bsaInstalled = true;
+ break;
+ }
+ }
+ if (!bsaInstalled) {
+ m_DataArchives->addArchive(profile, 0, invalidationBSAName());
+ dirty = true;
+ }
+
+ // create the dummy bsa if necessary
+ QString bsaFile = m_Game->dataDirectory().absoluteFilePath(invalidationBSAName());
+ if (!QFile::exists(bsaFile)) {
+ DummyBSA bsa(bsaVersion());
+ bsa.write(bsaFile);
+ dirty = true;
+ }
+
+ // write SInvalidationFile = "", if needed
+ QString sInvalidation = GameGamebryo::readIniValue(
+ iniFilePath, "Archive", "SInvalidationFile", "ArchiveInvalidation.txt");
+ if (sInvalidation != "") {
+ dirty = true;
+ if (!MOBase::WriteRegistryValue("Archive", "SInvalidationFile", "", iniFilePath)) {
+ qWarning("failed to activate BSA invalidation in \"%s\"",
+ qUtf8Printable(m_IniFileName));
+ }
+ }
+ } else {
+
+ // remove the dummy bsa from the archive string, if needed
+ QStringList archivesBefore = m_DataArchives->archives(profile);
+ for (const QString& archive : archivesBefore) {
+ if (isInvalidationBSA(archive)) {
+ m_DataArchives->removeArchive(profile, archive);
+ dirty = true;
+ }
+ }
+
+ // delete the dummy bsa, if needed
+ QString bsaFile = m_Game->dataDirectory().absoluteFilePath(invalidationBSAName());
+ if (QFile::exists(bsaFile)) {
+ MOBase::shellDeleteQuiet(bsaFile);
+ dirty = true;
+ }
+
+ // write SInvalidationFile = "ArchiveInvalidation.txt", if needed
+ QString sInvalidation2 =
+ GameGamebryo::readIniValue(iniFilePath, "Archive", "SInvalidationFile", "");
+ if (sInvalidation2 != "ArchiveInvalidation.txt") {
+ dirty = true;
+ if (!MOBase::WriteRegistryValue("Archive", "SInvalidationFile",
+ "ArchiveInvalidation.txt", iniFilePath)) {
+ qWarning("failed to activate BSA invalidation in \"%s\"",
+ qUtf8Printable(m_IniFileName));
+ }
+ }
+ }
+
+ return dirty;
+}
diff --git a/libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.h b/libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.h
new file mode 100644
index 0000000..efd3d7b
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.h
@@ -0,0 +1,36 @@
+#ifndef GAMEBRYOBSAINVALIDATION_H
+#define GAMEBRYOBSAINVALIDATION_H
+
+#include <QString>
+#include <bsainvalidation.h>
+#include <dataarchives.h>
+#include <memory>
+
+namespace MOBase
+{
+class IPluginGame;
+}
+
+class GamebryoBSAInvalidation : public MOBase::BSAInvalidation
+{
+public:
+ GamebryoBSAInvalidation(MOBase::DataArchives* dataArchives,
+ const QString& iniFilename, MOBase::IPluginGame const* game);
+
+ virtual bool isInvalidationBSA(const QString& bsaName) override;
+ virtual void deactivate(MOBase::IProfile* profile) override;
+ virtual void activate(MOBase::IProfile* profile) override;
+ virtual bool prepareProfile(MOBase::IProfile* profile) override;
+
+private:
+ virtual QString invalidationBSAName() const = 0;
+ virtual unsigned long
+ bsaVersion() const = 0; // 0x67 for oblivion, 0x68 for everything else
+
+private:
+ MOBase::DataArchives* m_DataArchives;
+ QString m_IniFileName;
+ MOBase::IPluginGame const* m_Game;
+};
+
+#endif // GAMEBRYOBSAINVALIDATION_H
diff --git a/libs/game_bethesda/src/gamebryo/gamebryodataarchives.cpp b/libs/game_bethesda/src/gamebryo/gamebryodataarchives.cpp
new file mode 100644
index 0000000..11e5408
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryodataarchives.cpp
@@ -0,0 +1,68 @@
+#include "gamebryodataarchives.h"
+
+#include <registry.h>
+#include <utility.h>
+
+#include "gamegamebryo.h"
+
+GamebryoDataArchives::GamebryoDataArchives(const GameGamebryo* game) : m_Game{game} {}
+
+QDir GamebryoDataArchives::gameDirectory() const
+{
+ return QDir(m_Game->gameDirectory()).absolutePath();
+}
+
+QDir GamebryoDataArchives::localGameDirectory() const
+{
+ return QDir(m_Game->myGamesPath()).absolutePath();
+}
+
+QStringList GamebryoDataArchives::getArchivesFromKey(const QString& iniFile,
+ const QString& key,
+ const int size) const
+{
+ QStringList result;
+
+ QString value = GameGamebryo::readIniValue(iniFile, "Archive", key, "");
+ if (!value.isEmpty()) {
+ result.append(value.split(','));
+ }
+
+ for (int i = 0; i < result.count(); ++i) {
+ result[i] = result[i].trimmed();
+ }
+ return result;
+}
+
+void GamebryoDataArchives::setArchivesToKey(const QString& iniFile, const QString& key,
+ const QString& value)
+{
+ if (!MOBase::WriteRegistryValue("Archive", key, value, iniFile)) {
+ qWarning("failed to set archives in \"%s\"", qUtf8Printable(iniFile));
+ }
+}
+
+void GamebryoDataArchives::addArchive(MOBase::IProfile* profile, int index,
+ const QString& archiveName)
+{
+ QStringList current = archives(profile);
+ if (current.contains(archiveName, Qt::CaseInsensitive)) {
+ return;
+ }
+
+ current.insert(index != INT_MAX ? index : current.size(), archiveName);
+
+ writeArchiveList(profile, current);
+}
+
+void GamebryoDataArchives::removeArchive(MOBase::IProfile* profile,
+ const QString& archiveName)
+{
+ QStringList current = archives(profile);
+ if (!current.contains(archiveName, Qt::CaseInsensitive)) {
+ return;
+ }
+ current.removeAll(archiveName);
+
+ writeArchiveList(profile, current);
+}
diff --git a/libs/game_bethesda/src/gamebryo/gamebryodataarchives.h b/libs/game_bethesda/src/gamebryo/gamebryodataarchives.h
new file mode 100644
index 0000000..13f4130
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryodataarchives.h
@@ -0,0 +1,37 @@
+#ifndef GAMEBRYODATAARCHIVES_H
+#define GAMEBRYODATAARCHIVES_H
+
+#include <QDir>
+
+#include "dataarchives.h"
+
+class GameGamebryo;
+
+class GamebryoDataArchives : public MOBase::DataArchives
+{
+
+public:
+ GamebryoDataArchives(const GameGamebryo* game);
+
+ virtual void addArchive(MOBase::IProfile* profile, int index,
+ const QString& archiveName) override;
+ virtual void removeArchive(MOBase::IProfile* profile,
+ const QString& archiveName) override;
+
+protected:
+ QDir gameDirectory() const;
+ QDir localGameDirectory() const;
+
+ QStringList getArchivesFromKey(const QString& iniFile, const QString& key,
+ int size = 256) const;
+ void setArchivesToKey(const QString& iniFile, const QString& key,
+ const QString& value);
+
+private:
+ const GameGamebryo* m_Game;
+
+ virtual void writeArchiveList(MOBase::IProfile* profile,
+ const QStringList& before) = 0;
+};
+
+#endif // GAMEBRYODATAARCHIVES_H
diff --git a/libs/game_bethesda/src/gamebryo/gamebryogameplugins.cpp b/libs/game_bethesda/src/gamebryo/gamebryogameplugins.cpp
new file mode 100644
index 0000000..1accc4f
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryogameplugins.cpp
@@ -0,0 +1,246 @@
+#include "gamebryogameplugins.h"
+#include <imodinterface.h>
+#include <iplugingame.h>
+#include <ipluginlist.h>
+#include <report.h>
+#include <safewritefile.h>
+#include <scopeguard.h>
+#include <utility.h>
+
+#include <QDateTime>
+#include <QDir>
+#include <QHash>
+#include <QString>
+#include <QStringEncoder>
+#include <QStringList>
+
+using MOBase::IOrganizer;
+using MOBase::IPluginList;
+using MOBase::reportError;
+using MOBase::SafeWriteFile;
+
+GamebryoGamePlugins::GamebryoGamePlugins(IOrganizer* organizer) : m_Organizer(organizer)
+{}
+
+void GamebryoGamePlugins::writePluginLists(const IPluginList* pluginList)
+{
+ if (!m_LastRead.isValid()) {
+ // attempt to write uninitialized plugin lists
+ return;
+ }
+
+ writePluginList(pluginList, m_Organizer->profile()->absolutePath() + "/plugins.txt");
+ writeLoadOrderList(pluginList,
+ m_Organizer->profile()->absolutePath() + "/loadorder.txt");
+
+ m_LastRead = QDateTime::currentDateTime();
+}
+
+void GamebryoGamePlugins::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 (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();
+}
+
+QStringList GamebryoGamePlugins::getLoadOrder()
+{
+ 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 (loadOrderIsNew || !pluginsIsNew) {
+ return readLoadOrderList(m_Organizer->pluginList(), loadOrderPath);
+ } else {
+ return readPluginList(m_Organizer->pluginList());
+ }
+}
+
+void GamebryoGamePlugins::writePluginList(const MOBase::IPluginList* pluginList,
+ const QString& filePath)
+{
+ return writeList(pluginList, filePath, false);
+}
+
+void GamebryoGamePlugins::writeLoadOrderList(const MOBase::IPluginList* pluginList,
+ const QString& filePath)
+{
+ return writeList(pluginList, filePath, true);
+}
+
+void GamebryoGamePlugins::writeList(const IPluginList* pluginList,
+ const QString& filePath, bool loadOrder)
+{
+ SafeWriteFile file(filePath);
+
+ QStringEncoder encoder = loadOrder
+ ? QStringEncoder(QStringConverter::Encoding::Utf8)
+ : QStringEncoder(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);
+ });
+
+ 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 {
+ 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."));
+ }
+
+ if (writtenCount == 0) {
+ qWarning("plugin list would be empty, this is almost certainly wrong. Not "
+ "saving.");
+ } else {
+ file->commit();
+ }
+}
+
+QStringList GamebryoGamePlugins::readLoadOrderList(MOBase::IPluginList* pluginList,
+ const QString& filePath)
+{
+ QStringList pluginNames = organizer()->managedGame()->primaryPlugins();
+
+ std::set<QString> pluginLookup;
+ for (auto&& name : pluginNames) {
+ pluginLookup.insert(name.toLower());
+ }
+
+ const auto b = MOBase::forEachLineInFile(filePath, [&](QString s) {
+ if (!pluginLookup.contains(s.toLower())) {
+ pluginLookup.insert(s);
+ pluginNames.push_back(std::move(s));
+ }
+ });
+
+ if (!b) {
+ return readPluginList(pluginList);
+ }
+
+ return pluginNames;
+}
+
+QStringList GamebryoGamePlugins::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();
+ QStringList pluginsClone(plugins);
+ // Do not sort the primary plugins. Their load order should be locked as defined in
+ // "primaryPlugins".
+ for (const auto& plugin : pluginsClone) {
+ if (primary.contains(plugin, Qt::CaseInsensitive))
+ plugins.removeAll(plugin);
+ }
+
+ // Always use filetime loadorder to get the actual load order
+ QHash<QString, QDateTime> modificationTimes;
+ modificationTimes.reserve(plugins.size());
+ for (const auto& pluginName : plugins) {
+ MOBase::IModInterface* mod =
+ organizer()->modList()->getMod(pluginList->origin(pluginName));
+ QDir dir = mod ? mod->absolutePath() : organizer()->managedGame()->dataDirectory();
+ QString absPath = dir.absoluteFilePath(pluginName);
+ modificationTimes.insert(pluginName, QFileInfo(absPath).lastModified());
+ }
+
+ std::ranges::sort(
+ plugins, [&modificationTimes](const QString& lhs, const QString& rhs) {
+ return modificationTimes.value(lhs) < modificationTimes.value(rhs);
+ });
+
+ // 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([&]() {
+ 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;
+ }
+
+ QStringList activePlugins;
+ if (pluginsTxtExists) {
+ while (!file.atEnd()) {
+ QByteArray line = file.readLine();
+ QString pluginName;
+ if ((line.size() > 0) && (line.at(0) != '#')) {
+ QStringEncoder encoder(QStringConverter::Encoding::System);
+ pluginName = encoder.encode(line.trimmed().constData());
+ }
+ if (pluginName.size() > 0) {
+ pluginList->setState(pluginName, IPluginList::STATE_ACTIVE);
+ activePlugins.push_back(pluginName);
+ }
+ }
+
+ for (const auto& pluginName : plugins) {
+ if (!activePlugins.contains(pluginName, Qt::CaseInsensitive)) {
+ pluginList->setState(pluginName, IPluginList::STATE_INACTIVE);
+ }
+ }
+ } else {
+ for (const QString& pluginName : plugins) {
+ pluginList->setState(pluginName, IPluginList::STATE_INACTIVE);
+ }
+ }
+
+ return primary + plugins;
+}
diff --git a/libs/game_bethesda/src/gamebryo/gamebryogameplugins.h b/libs/game_bethesda/src/gamebryo/gamebryogameplugins.h
new file mode 100644
index 0000000..ca70c75
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryogameplugins.h
@@ -0,0 +1,38 @@
+#ifndef GAMEBRYOGAMEPLUGINS_H
+#define GAMEBRYOGAMEPLUGINS_H
+
+#include <QDateTime>
+#include <QStringList>
+#include <gameplugins.h>
+#include <imoinfo.h>
+
+class GamebryoGamePlugins : public MOBase::GamePlugins
+{
+public:
+ GamebryoGamePlugins(MOBase::IOrganizer* organizer);
+
+ virtual void writePluginLists(const MOBase::IPluginList* pluginList) override;
+ virtual void readPluginLists(MOBase::IPluginList* pluginList) override;
+ virtual QStringList getLoadOrder() override;
+
+protected:
+ MOBase::IOrganizer* organizer() const { return m_Organizer; }
+
+ virtual void writePluginList(const MOBase::IPluginList* pluginList,
+ const QString& filePath);
+ virtual void writeLoadOrderList(const MOBase::IPluginList* pluginList,
+ const QString& filePath);
+ virtual QStringList readLoadOrderList(MOBase::IPluginList* pluginList,
+ const QString& filePath);
+ virtual QStringList readPluginList(MOBase::IPluginList* pluginList);
+
+protected:
+ MOBase::IOrganizer* m_Organizer;
+ QDateTime m_LastRead;
+
+private:
+ void writeList(const MOBase::IPluginList* pluginList, const QString& filePath,
+ bool loadOrder);
+};
+
+#endif // GAMEBRYOGAMEPLUGINS_H
diff --git a/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp b/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp
new file mode 100644
index 0000000..e4df0ea
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp
@@ -0,0 +1,138 @@
+/*
+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 "gamebryolocalsavegames.h"
+#include "registry.h"
+#include <QtDebug>
+#include <iprofile.h>
+#include <stddef.h>
+#include <string>
+
+#include <QSettings>
+
+#include "gamegamebryo.h"
+
+// Helper to remove a key from a Bethesda-style INI file
+static void removeIniValue(const QString& iniFile, const QString& section,
+ const QString& key)
+{
+ QSettings settings(iniFile, QSettings::IniFormat);
+ settings.beginGroup(section);
+ settings.remove(key);
+ settings.endGroup();
+ settings.sync();
+}
+
+GamebryoLocalSavegames::GamebryoLocalSavegames(const GameGamebryo* game,
+ const QString& iniFileName)
+ : m_Game{game}, m_IniFileName(iniFileName)
+{}
+
+MappingType GamebryoLocalSavegames::mappings(const QDir& profileSaveDir) const
+{
+ return {{profileSaveDir.absolutePath(), localSavesDirectory().absolutePath(), true,
+ true}};
+}
+
+QString GamebryoLocalSavegames::localSavesDummy() const
+{
+ return "__MO_Saves\\";
+}
+
+QDir GamebryoLocalSavegames::localSavesDirectory() const
+{
+ return QDir(m_Game->myGamesPath()).absoluteFilePath(localSavesDummy());
+}
+
+QDir GamebryoLocalSavegames::localGameDirectory() const
+{
+ return QDir(m_Game->myGamesPath()).absolutePath();
+}
+
+bool GamebryoLocalSavegames::prepareProfile(MOBase::IProfile* profile)
+{
+ bool enable = profile->localSavesEnabled();
+
+ QString basePath = profile->localSettingsEnabled()
+ ? profile->absolutePath()
+ : localGameDirectory().absolutePath();
+ QString iniFilePath = basePath + "/" + m_IniFileName;
+ QString saveIni = profile->absolutePath() + "/" + "savepath.ini";
+
+ // Get the current sLocalSavePath
+ QString currentPath =
+ GameGamebryo::readIniValue(iniFilePath, "General", "sLocalSavePath", "SKIP_ME");
+ bool alreadyEnabled = (currentPath == localSavesDummy());
+
+ // Get the current bUseMyGamesDirectory
+ QString currentMyGames = GameGamebryo::readIniValue(
+ iniFilePath, "General", "bUseMyGamesDirectory", "SKIP_ME");
+
+ // Create the __MO_Saves directory if local saves are enabled and it doesn't exist
+ if (enable) {
+ QDir saves = localSavesDirectory();
+ if (!saves.exists()) {
+ saves.mkdir(".");
+ }
+ }
+
+ // Set the path to __MO_Saves if it's not already
+ if (enable && !alreadyEnabled) {
+ // If the path is not blank, save it to savepath.ini
+ if (currentPath != "SKIP_ME") {
+ MOBase::WriteRegistryValue("General", "sLocalSavePath", currentPath, saveIni);
+ }
+ if (currentMyGames != "SKIP_ME") {
+ MOBase::WriteRegistryValue("General", "bUseMyGamesDirectory", currentMyGames,
+ saveIni);
+ }
+ MOBase::WriteRegistryValue("General", "sLocalSavePath", localSavesDummy(),
+ iniFilePath);
+ MOBase::WriteRegistryValue("General", "bUseMyGamesDirectory", "1", iniFilePath);
+ }
+
+ // Get rid of the local saves setting if it's still there
+ if (!enable && alreadyEnabled) {
+ // If savepath.ini exists, use it and delete it
+ if (QFile::exists(saveIni)) {
+ QString savedPath =
+ GameGamebryo::readIniValue(saveIni, "General", "sLocalSavePath", "DELETE_ME");
+ QString savedMyGames = GameGamebryo::readIniValue(
+ saveIni, "General", "bUseMyGamesDirectory", "DELETE_ME");
+ if (savedPath != "DELETE_ME") {
+ MOBase::WriteRegistryValue("General", "sLocalSavePath", savedPath, iniFilePath);
+ } else {
+ removeIniValue(iniFilePath, "General", "sLocalSavePath");
+ }
+ if (savedMyGames != "DELETE_ME") {
+ MOBase::WriteRegistryValue("General", "bUseMyGamesDirectory", savedMyGames,
+ iniFilePath);
+ } else {
+ removeIniValue(iniFilePath, "General", "bUseMyGamesDirectory");
+ }
+ QFile::remove(saveIni);
+ }
+ // Otherwise just delete the setting
+ else {
+ removeIniValue(iniFilePath, "General", "sLocalSavePath");
+ removeIniValue(iniFilePath, "General", "bUseMyGamesDirectory");
+ }
+ }
+
+ return enable != alreadyEnabled;
+}
diff --git a/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.h b/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.h
new file mode 100644
index 0000000..e5f2a92
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.h
@@ -0,0 +1,54 @@
+/*
+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 GAMEBRYOLOCALSAVEGAMES_H
+#define GAMEBRYOLOCALSAVEGAMES_H
+
+#include <localsavegames.h>
+
+#include <QDir>
+#include <QString>
+
+class GameGamebryo;
+
+class GamebryoLocalSavegames : public MOBase::LocalSavegames
+{
+
+public:
+ GamebryoLocalSavegames(const GameGamebryo* game, const QString& iniFileName);
+
+ virtual MappingType mappings(const QDir& profileSaveDir) const override;
+ virtual bool prepareProfile(MOBase::IProfile* profile) override;
+
+protected:
+ // return the path from the local game directory to the local saves folder
+ //
+ // this is virtual so game plugins for complete game overhauld (Enderal, Nehrim, etc.)
+ // can override it properly
+ //
+ virtual QString localSavesDummy() const;
+
+ QDir localSavesDirectory() const;
+ QDir localGameDirectory() const;
+
+private:
+ const GameGamebryo* m_Game;
+ QString m_IniFileName;
+};
+
+#endif // GAMEBRYOLOCALSAVEGAMES_H
diff --git a/libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.cpp b/libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.cpp
new file mode 100644
index 0000000..b5bace6
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.cpp
@@ -0,0 +1,76 @@
+#include <ifiletree.h>
+
+#include "gamebryomoddatachecker.h"
+
+/**
+ * @return the list of possible folder names in data.
+ */
+auto GamebryoModDataChecker::possibleFolderNames() const -> const FileNameSet&
+{
+ static FileNameSet result{"fonts",
+ "interface",
+ "menus",
+ "meshes",
+ "music",
+ "scripts",
+ "shaders",
+ "sound",
+ "strings",
+ "textures",
+ "trees",
+ "video",
+ "facegen",
+ "materials",
+ "skse",
+ "obse",
+ "mwse",
+ "nvse",
+ "fose",
+ "f4se",
+ "distantlod",
+ "asi",
+ "SkyProc Patchers",
+ "Tools",
+ "MCM",
+ "icons",
+ "bookart",
+ "distantland",
+ "mits",
+ "splash",
+ "dllplugins",
+ "CalienteTools",
+ "NetScriptFramework",
+ "shadersfx"};
+ return result;
+}
+
+/**
+ * @return the extensions of possible files in data.
+ */
+auto GamebryoModDataChecker::possibleFileExtensions() const -> const FileNameSet&
+{
+ static FileNameSet result{"esp", "esm", "esl", "bsa", "ba2", "modgroups", "ini"};
+ return result;
+}
+
+GamebryoModDataChecker::GamebryoModDataChecker(const GameGamebryo* game) : m_Game(game)
+{}
+
+GamebryoModDataChecker::CheckReturn GamebryoModDataChecker::dataLooksValid(
+ std::shared_ptr<const MOBase::IFileTree> fileTree) const
+{
+ auto& folders = possibleFolderNames();
+ auto& suffixes = possibleFileExtensions();
+ for (auto entry : *fileTree) {
+ if (entry->isDir()) {
+ if (folders.count(entry->name()) > 0) {
+ return CheckReturn::VALID;
+ }
+ } else {
+ if (suffixes.count(entry->suffix()) > 0) {
+ return CheckReturn::VALID;
+ }
+ }
+ }
+ return CheckReturn::INVALID;
+}
diff --git a/libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.h b/libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.h
new file mode 100644
index 0000000..e0b0733
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.h
@@ -0,0 +1,46 @@
+#ifndef GAMEBRYO_MODATACHECKER_H
+#define GAMEBRYO_MODATACHECKER_H
+
+#include <ifiletree.h>
+#include <moddatachecker.h>
+
+class GameGamebryo;
+
+/**
+ * @brief ModDataChecker for GameBryo games that look at folder and files in the "data"
+ * directory.
+ *
+ * The default implementation is game-agnostic and uses the list of folders and file
+ * extensions that were used before the ModDataChecker feature was added. It is possible
+ * to inherit the class to provide custom list of folders or filenames.
+ */
+class GamebryoModDataChecker : public MOBase::ModDataChecker
+{
+public:
+ /**
+ * @brief Construct a new mod-data checker for GameBryo games.
+ */
+ GamebryoModDataChecker(const GameGamebryo* game);
+
+ virtual CheckReturn
+ dataLooksValid(std::shared_ptr<const MOBase::IFileTree> fileTree) const override;
+
+protected:
+ GameGamebryo const* const m_Game;
+
+ using FileNameSet = std::set<QString, MOBase::FileNameComparator>;
+
+ const GameGamebryo* game() const { return m_Game; }
+
+ /**
+ * @return the list of possible folder names in data.
+ */
+ virtual const FileNameSet& possibleFolderNames() const;
+
+ /**
+ * @return the extensions of possible files in data.
+ */
+ virtual const FileNameSet& possibleFileExtensions() const;
+};
+
+#endif // GAMEBRYO_MODATACHECKER_H
diff --git a/libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.cpp b/libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.cpp
new file mode 100644
index 0000000..85b66b0
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.cpp
@@ -0,0 +1,116 @@
+#include "gamebryomoddatacontent.h"
+
+#include <igamefeatures.h>
+#include <scriptextender.h>
+
+GamebryoModDataContent::GamebryoModDataContent(
+ MOBase::IGameFeatures const* gameFeatures)
+ : m_GameFeatures(gameFeatures), m_Enabled(CONTENT_MODGROUP + 1, true)
+{}
+
+std::vector<GamebryoModDataContent::Content>
+GamebryoModDataContent::getAllContents() const
+{
+ static std::vector<Content> GAMEBRYO_CONTENTS{
+ {CONTENT_PLUGIN, QT_TR_NOOP("Plugins (ESP/ESM/ESL)"), ":/MO/gui/content/plugin"},
+ {CONTENT_OPTIONAL, QT_TR_NOOP("Optional Plugins"), "", true},
+ {CONTENT_INTERFACE, QT_TR_NOOP("Interface"), ":/MO/gui/content/interface"},
+ {CONTENT_MESH, QT_TR_NOOP("Meshes"), ":/MO/gui/content/mesh"},
+ {CONTENT_BSA, QT_TR_NOOP("Bethesda Archive"), ":/MO/gui/content/bsa"},
+ {CONTENT_SCRIPT, QT_TR_NOOP("Scripts (Papyrus)"), ":/MO/gui/content/script"},
+ {CONTENT_SKSE, QT_TR_NOOP("Script Extender Plugin"), ":/MO/gui/content/skse"},
+ {CONTENT_SKSE_FILES, QT_TR_NOOP("Script Extender Files"), "", true},
+ {CONTENT_SKYPROC, QT_TR_NOOP("SkyProc Patcher"), ":/MO/gui/content/skyproc"},
+ {CONTENT_SOUND, QT_TR_NOOP("Sound or Music"), ":/MO/gui/content/sound"},
+ {CONTENT_TEXTURE, QT_TR_NOOP("Textures"), ":/MO/gui/content/texture"},
+ {CONTENT_MCM, QT_TR_NOOP("MCM Configuration"), ":/MO/gui/content/menu"},
+ {CONTENT_INI, QT_TR_NOOP("INI Files"), ":/MO/gui/content/inifile"},
+ {CONTENT_FACEGEN, QT_TR_NOOP("FaceGen Data"), ":/MO/gui/content/facegen"},
+ {CONTENT_MODGROUP, QT_TR_NOOP("ModGroup Files"), ":/MO/gui/content/modgroup"}};
+
+ // Copy the list of enabled contents:
+ std::vector<Content> contents;
+ std::copy_if(std::begin(GAMEBRYO_CONTENTS), std::end(GAMEBRYO_CONTENTS),
+ std::back_inserter(contents), [this](auto e) {
+ return m_Enabled[e.id()];
+ });
+ return contents;
+}
+
+std::vector<int> GamebryoModDataContent::getContentsFor(
+ std::shared_ptr<const MOBase::IFileTree> fileTree) const
+{
+ std::vector<int> contents;
+
+ for (auto e : *fileTree) {
+ if (e->isFile()) {
+ auto suffix = e->suffix().toLower();
+ if (m_Enabled[CONTENT_PLUGIN] &&
+ (suffix == "esp" || suffix == "esm" || suffix == "esl")) {
+ contents.push_back(CONTENT_PLUGIN);
+ } else if (m_Enabled[CONTENT_BSA] && (suffix == "bsa" || suffix == "ba2")) {
+ contents.push_back(CONTENT_BSA);
+ } else if (m_Enabled[CONTENT_INI] && suffix == "ini" &&
+ e->compare("meta.ini") != 0) {
+ contents.push_back(CONTENT_INI);
+ } else if (m_Enabled[CONTENT_MODGROUP] && suffix == "modgroups") {
+ contents.push_back(CONTENT_MODGROUP);
+ }
+ } else {
+ if (m_Enabled[CONTENT_TEXTURE] &&
+ (e->compare("textures") == 0 || e->compare("icons") == 0 ||
+ e->compare("bookart") == 0)) {
+ contents.push_back(CONTENT_TEXTURE);
+ } else if (m_Enabled[CONTENT_MESH] && e->compare("meshes") == 0) {
+ contents.push_back(CONTENT_MESH);
+ } else if (m_Enabled[CONTENT_INTERFACE] &&
+ (e->compare("interface") == 0 || e->compare("menus") == 0)) {
+ contents.push_back(CONTENT_INTERFACE);
+ } else if (m_Enabled[CONTENT_SOUND] && e->compare("music") == 0 ||
+ e->compare("sound") == 0) {
+ contents.push_back(CONTENT_SOUND);
+ } else if (m_Enabled[CONTENT_SCRIPT] && e->compare("scripts") == 0) {
+ contents.push_back(CONTENT_SCRIPT);
+ } else if (m_Enabled[CONTENT_SKYPROC] && e->compare("SkyProc Patchers") == 0) {
+ contents.push_back(CONTENT_SKYPROC);
+ } else if (m_Enabled[CONTENT_MCM] && e->compare("MCM") == 0) {
+ contents.push_back(CONTENT_MCM);
+ } else if (m_Enabled[CONTENT_OPTIONAL] && e->compare("Optional") == 0 &&
+ e->astree()->size() > 0) {
+ contents.push_back(CONTENT_OPTIONAL);
+ }
+ }
+ }
+
+ if (m_Enabled[CONTENT_FACEGEN]) {
+ auto e1 = fileTree->findDirectory("meshes/actors/character/facegendata");
+ if (e1) {
+ contents.push_back(CONTENT_FACEGEN);
+ } else {
+ auto e2 = fileTree->findDirectory("textures/actors/character/facegendata");
+ if (e2) {
+ contents.push_back(CONTENT_FACEGEN);
+ }
+ }
+ }
+
+ auto extender = m_GameFeatures->gameFeature<MOBase::ScriptExtender>();
+ if (extender != nullptr) {
+ auto e = fileTree->findDirectory(extender->PluginPath());
+ if (e) {
+ if (m_Enabled[CONTENT_SKSE_FILES]) {
+ contents.push_back(CONTENT_SKSE_FILES);
+ }
+ if (m_Enabled[CONTENT_SKSE]) {
+ for (auto f : *e) {
+ if (f->hasSuffix("dll")) {
+ contents.push_back(CONTENT_SKSE);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return contents;
+}
diff --git a/libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.h b/libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.h
new file mode 100644
index 0000000..be02331
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.h
@@ -0,0 +1,75 @@
+#ifndef GAMEBRYO_MODDATACONTENT_H
+#define GAMEBRYO_MODDATACONTENT_H
+
+#include <ifiletree.h>
+#include <moddatacontent.h>
+
+namespace MOBase
+{
+class IGameFeatures;
+}
+
+/**
+ * @brief ModDataContent for GameBryo games.
+ *
+ */
+class GamebryoModDataContent : public MOBase::ModDataContent
+{
+protected:
+ /**
+ * Note: These are used to index m_Enabled so should have standard
+ * enum values, not custom ones.
+ */
+ enum EContent
+ {
+ CONTENT_PLUGIN,
+ CONTENT_OPTIONAL,
+ CONTENT_TEXTURE,
+ CONTENT_MESH,
+ CONTENT_BSA,
+ CONTENT_INTERFACE,
+ CONTENT_SOUND,
+ CONTENT_SCRIPT,
+ CONTENT_SKSE,
+ CONTENT_SKSE_FILES,
+ CONTENT_SKYPROC,
+ CONTENT_MCM,
+ CONTENT_INI,
+ CONTENT_FACEGEN,
+ CONTENT_MODGROUP
+ };
+
+ /**
+ * This is the first value that can be used for game-specific contents.
+ */
+ constexpr static auto CONTENT_NEXT_VALUE = CONTENT_MODGROUP + 1;
+
+public:
+ /**
+ *
+ */
+ GamebryoModDataContent(const MOBase::IGameFeatures* gameFeatures);
+
+ /**
+ * @return the list of all possible contents for the corresponding game.
+ */
+ virtual std::vector<Content> getAllContents() const override;
+
+ /**
+ * @brief Retrieve the list of contents in the given tree.
+ *
+ * @param fileTree The tree corresponding to the mod to retrieve contents for.
+ *
+ * @return the IDs of the content in the given tree.
+ */
+ virtual std::vector<int>
+ getContentsFor(std::shared_ptr<const MOBase::IFileTree> fileTree) const override;
+
+protected:
+ MOBase::IGameFeatures const* const m_GameFeatures;
+
+ // List of enabled contents:
+ std::vector<bool> m_Enabled;
+};
+
+#endif // GAMEBRYO_MODDATACONTENT_H
diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp b/libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp
new file mode 100644
index 0000000..523df0d
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp
@@ -0,0 +1,607 @@
+#include "gamebryosavegame.h"
+
+#include "iplugingame.h"
+#include "log.h"
+#include "scriptextender.h"
+
+#include <QDate>
+#include <QFile>
+#include <QFileInfo>
+#include <QScopedArrayPointer>
+#include <QTime>
+
+#ifdef _WIN32
+#include <Windows.h>
+#endif
+#include <lz4.h>
+#include <zlib.h>
+
+#include <stdexcept>
+#include <vector>
+
+#include "gamegamebryo.h"
+#include "imoinfo.h"
+
+#define CHUNK 16384
+
+GamebryoSaveGame::GamebryoSaveGame(QString const& file, GameGamebryo const* game,
+ bool const lightEnabled, bool const mediumEnabled)
+ : m_FileName(file), m_CreationTime(QFileInfo(file).lastModified()), m_Game(game),
+ m_MediumEnabled(mediumEnabled), m_LightEnabled(lightEnabled),
+ m_DataFields([this]() {
+ return fetchDataFields();
+ })
+{}
+
+GamebryoSaveGame::~GamebryoSaveGame() {}
+
+QString GamebryoSaveGame::getFilepath() const
+{
+ return m_FileName;
+}
+
+QDateTime GamebryoSaveGame::getCreationTime() const
+{
+ return m_CreationTime;
+}
+
+QString GamebryoSaveGame::getName() const
+{
+ return QObject::tr("%1, #%2, Level %3, %4")
+ .arg(m_PCName)
+ .arg(m_SaveNumber)
+ .arg(m_PCLevel)
+ .arg(m_PCLocation);
+}
+
+QString GamebryoSaveGame::getSaveGroupIdentifier() const
+{
+ return m_PCName;
+}
+
+QStringList GamebryoSaveGame::allFiles() const
+{
+ // This returns all valid files associated with this game
+ QStringList res = {m_FileName};
+ auto e = m_Game->m_Organizer->gameFeatures()->gameFeature<MOBase::ScriptExtender>();
+ if (e != nullptr) {
+ QFileInfo file(m_FileName);
+ QFileInfo SEfile(file.absolutePath() + "/" + file.completeBaseName() + "." +
+ m_Game->savegameSEExtension());
+ if (SEfile.exists()) {
+ res.push_back(SEfile.absoluteFilePath());
+ }
+ }
+ return res;
+}
+
+bool GamebryoSaveGame::hasScriptExtenderFile() const
+{
+ QFileInfo file(m_FileName);
+ QFileInfo SEfile(file.absolutePath() + "/" + file.completeBaseName() + "." +
+ m_Game->savegameSEExtension());
+ return SEfile.exists();
+}
+
+void GamebryoSaveGame::setCreationTime(_SYSTEMTIME const& ctime)
+{
+ QDate date;
+ date.setDate(ctime.wYear, ctime.wMonth, ctime.wDay);
+ QTime time;
+ time.setHMS(ctime.wHour, ctime.wMinute, ctime.wSecond, ctime.wMilliseconds);
+
+ m_CreationTime = QDateTime(date, time, Qt::UTC);
+}
+
+GamebryoSaveGame::FileWrapper::FileWrapper(QString const& filepath,
+ QString const& expected)
+ : m_File(filepath), m_HasFieldMarkers(false),
+ m_PluginString(StringType::TYPE_WSTRING),
+ m_PluginStringFormat(StringFormat::UTF8), m_NextChunk(0)
+{
+ if (!m_File.open(QIODevice::ReadOnly)) {
+ throw std::runtime_error(
+ QObject::tr("failed to open %1").arg(filepath).toUtf8().constData());
+ }
+
+ std::vector<char> fileID(expected.length() + 1);
+ m_File.read(fileID.data(), expected.length());
+ fileID[expected.length()] = '\0';
+
+ QString id(fileID.data());
+ if (expected != id) {
+ throw std::runtime_error(
+ QObject::tr("wrong file format - expected %1 got \'%2\' for %3")
+ .arg(expected)
+ .arg(id)
+ .arg(filepath)
+ .toUtf8()
+ .constData());
+ }
+}
+
+void GamebryoSaveGame::FileWrapper::setHasFieldMarkers(bool state)
+{
+ m_HasFieldMarkers = state;
+}
+
+void GamebryoSaveGame::FileWrapper::setPluginString(StringType type)
+{
+ m_PluginString = type;
+}
+
+void GamebryoSaveGame::FileWrapper::setPluginStringFormat(StringFormat type)
+{
+ m_PluginStringFormat = type;
+}
+
+void GamebryoSaveGame::FileWrapper::readQDataStream(QDataStream& data, void* buff,
+ std::size_t length)
+{
+ int read = data.readRawData(static_cast<char*>(buff), static_cast<int>(length));
+ bool result = true;
+ if (read != length && m_CompressionType == 1) {
+ bool result = readNextChunk();
+ if (result) {
+ read += data.readRawData(static_cast<char*>(buff) + read,
+ static_cast<int>(length - read));
+ }
+ }
+ if (read != length || !result) {
+ throw std::runtime_error("unexpected end of file");
+ }
+}
+
+template <typename T>
+void GamebryoSaveGame::FileWrapper::readQDataStream(QDataStream& data, T& value)
+{
+ static_assert(std::is_trivial_v<T> && std::is_standard_layout_v<T>);
+ readQDataStream(data, &value, sizeof(T));
+}
+
+void GamebryoSaveGame::FileWrapper::skipQDataStream(QDataStream& data,
+ std::size_t length)
+{
+ int skip = data.skipRawData(static_cast<int>(length));
+ bool result = true;
+ if (skip != length && m_CompressionType == 1) {
+ result = readNextChunk();
+ if (result) {
+ skip += data.skipRawData(static_cast<int>(length - skip));
+ }
+ }
+ if (skip != length || !result) {
+ throw std::runtime_error("unexpected end of file");
+ }
+}
+
+template <>
+void GamebryoSaveGame::FileWrapper::read<QString>(QString& value)
+{
+ if (m_CompressionType == 0) {
+ unsigned short length;
+ if (m_PluginString == StringType::TYPE_BSTRING ||
+ m_PluginString == StringType::TYPE_BZSTRING) {
+ unsigned char len;
+ read(len);
+ length = m_PluginString == StringType::TYPE_BZSTRING ? len + 1 : len;
+ } else {
+ read(length);
+ }
+
+ if (m_HasFieldMarkers) {
+ skip<char>();
+ }
+
+ QByteArray buffer;
+ buffer.resize(length);
+
+ read(buffer.data(),
+ m_PluginString == StringType::TYPE_BZSTRING ? length - 1 : length);
+
+ if (m_PluginString == StringType::TYPE_BZSTRING)
+ buffer[length - 1] = '\0';
+
+ if (m_HasFieldMarkers) {
+ skip<char>();
+ }
+
+ if (m_PluginStringFormat == StringFormat::UTF8)
+ value = QString::fromUtf8(buffer.constData());
+ else
+ value = QString::fromLocal8Bit(buffer.constData());
+ } else if (m_CompressionType == 1 || m_CompressionType == 2) {
+ unsigned short length;
+ if (m_PluginString == StringType::TYPE_BSTRING ||
+ m_PluginString == StringType::TYPE_BZSTRING) {
+ unsigned char len;
+ readQDataStream(*m_Data, len);
+ length = m_PluginString == StringType::TYPE_BZSTRING ? len + 1 : len;
+ } else {
+ readQDataStream(*m_Data, length);
+ }
+
+ if (m_HasFieldMarkers) {
+ skip<char>();
+ }
+
+ QByteArray buffer;
+ buffer.resize(length);
+
+ readQDataStream(*m_Data, buffer.data(),
+ m_PluginString == StringType::TYPE_BZSTRING ? length - 1 : length);
+
+ if (m_PluginString == StringType::TYPE_BZSTRING)
+ buffer[length - 1] = '\0';
+
+ if (m_HasFieldMarkers) {
+ m_Data->skipRawData(1);
+ }
+
+ if (m_PluginStringFormat == StringFormat::UTF8)
+ value = QString::fromUtf8(buffer.constData());
+ else
+ value = QString::fromLocal8Bit(buffer.constData());
+ } else {
+ MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown "
+ "Compressed\" with your savefile attached");
+ }
+}
+
+void GamebryoSaveGame::FileWrapper::read(void* buff, std::size_t length)
+{
+ int read = m_File.read(static_cast<char*>(buff), length);
+ if (read != length) {
+ throw std::runtime_error("unexpected end of file");
+ }
+}
+
+QImage GamebryoSaveGame::FileWrapper::readImage(int scale, bool alpha)
+{
+ uint32_t width;
+ read(width);
+ uint32_t height;
+ read(height);
+ return readImage(width, height, scale, alpha);
+}
+
+QImage GamebryoSaveGame::FileWrapper::readImage(uint32_t width, uint32_t height, int scale,
+ bool alpha)
+{
+ int bpp = alpha ? 4 : 3;
+ QScopedArrayPointer<unsigned char> buffer(new unsigned char[width * height * bpp]);
+ read(buffer.data(), width * height * bpp);
+ QImage image(buffer.data(), width, height,
+ alpha ? QImage::Format_RGBA8888_Premultiplied : QImage::Format_RGB888);
+
+ // We need to copy the image here because QImage does not make a copy of the
+ // buffer when constructed.
+ if (scale != 0) {
+ return image.copy().scaledToWidth(scale);
+ } else {
+ return image.copy();
+ }
+}
+
+void GamebryoSaveGame::FileWrapper::setCompressionType(uint16_t compressionType)
+{
+ m_CompressionType = compressionType;
+}
+
+void GamebryoSaveGame::FileWrapper::closeCompressedData()
+{
+ if (m_CompressionType == 0) {
+ } else if (m_CompressionType == 1 || m_CompressionType == 2) {
+ m_NextChunk = 0;
+ m_UncompressedSize = 0;
+ m_Data->device()->close();
+ delete m_Data;
+ } else
+ MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown "
+ "Compressed\" with your savefile attached");
+}
+
+bool GamebryoSaveGame::FileWrapper::openCompressedData(int bytesToIgnore)
+{
+ if (m_CompressionType == 0) {
+ if (bytesToIgnore > 0) // Just to make certain
+ skip<char>(bytesToIgnore);
+ return false;
+ } else if (m_CompressionType == 1) {
+ read(m_NextChunk);
+ read(m_UncompressedSize);
+ QByteArray placeholder;
+ m_Data = new QDataStream(placeholder);
+ bool result = readNextChunk();
+ if (result)
+ skipQDataStream(*m_Data, bytesToIgnore);
+ return result;
+ } else if (m_CompressionType == 2) {
+ uint32_t uncompressedSize;
+ read(uncompressedSize);
+ uint32_t compressedSize;
+ read(compressedSize);
+ QByteArray compressed;
+ compressed.resize(compressedSize);
+ read(compressed.data(), compressedSize);
+ QByteArray decompressed;
+ decompressed.resize(uncompressedSize);
+ LZ4_decompress_safe_partial(compressed.data(), decompressed.data(), compressedSize,
+ uncompressedSize, uncompressedSize);
+ compressed.clear();
+
+ m_Data = new QDataStream(decompressed);
+ skipQDataStream(*m_Data, bytesToIgnore);
+
+ return true;
+ } else {
+ MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown "
+ "Compressed\" with your savefile attached");
+ return false;
+ }
+}
+
+bool GamebryoSaveGame::FileWrapper::readNextChunk()
+{
+ uint32_t have;
+ uint64_t read = 0;
+ std::unique_ptr<char[]> inBuffer = std::make_unique<char[]>(CHUNK);
+ std::unique_ptr<char[]> outBuffer = std::make_unique<char[]>(CHUNK);
+ QByteArray finalData;
+ m_Data->device()->close();
+ delete m_Data;
+ z_stream stream{};
+ try {
+ stream.zalloc = Z_NULL;
+ stream.zfree = Z_NULL;
+ stream.opaque = Z_NULL;
+ stream.avail_in = 0;
+ stream.next_in = Z_NULL;
+ if (m_NextChunk >= m_File.size() || finalData.size() == m_UncompressedSize)
+ return false;
+ m_File.seek(m_NextChunk);
+ int zlibRet = inflateInit2(&stream, 15 + 32);
+ if (zlibRet != Z_OK) {
+ return false;
+ }
+ do {
+ stream.avail_in = m_File.read(inBuffer.get(), CHUNK);
+ read += stream.avail_in;
+ if (!m_File.isReadable()) {
+ (void)inflateEnd(&stream);
+ return false;
+ }
+ if (stream.avail_in == 0)
+ break;
+ stream.next_in = reinterpret_cast<Bytef*>(inBuffer.get());
+ do {
+ stream.avail_out = CHUNK;
+ stream.next_out = reinterpret_cast<Bytef*>(outBuffer.get());
+ zlibRet = inflate(&stream, Z_NO_FLUSH);
+ if ((zlibRet != Z_OK) && (zlibRet != Z_STREAM_END) &&
+ (zlibRet != Z_BUF_ERROR)) {
+ return false;
+ }
+ have = CHUNK - stream.avail_out;
+ finalData += QByteArray::fromRawData(outBuffer.get(), have);
+ } while (stream.avail_out == 0);
+ read -= stream.avail_in;
+ } while (zlibRet != Z_STREAM_END);
+ inflateEnd(&stream);
+ uint64_t remainder = (m_NextChunk + read) % 16;
+ uint64_t next = m_NextChunk + read + 16 - (remainder == 0 ? 16 : remainder);
+ m_NextChunk = next;
+ } catch (const std::exception&) {
+ inflateEnd(&stream);
+ return false;
+ }
+ m_Data = new QDataStream(finalData);
+ return true;
+}
+
+uint8_t GamebryoSaveGame::FileWrapper::readChar(int bytesToIgnore)
+{
+ if (m_CompressionType == 0) {
+ if (bytesToIgnore > 0) // Just to make certain
+ skip<char>(bytesToIgnore);
+ uint8_t version;
+ read(version);
+ return version;
+ } else if (m_CompressionType == 1 || m_CompressionType == 2) {
+ // decompression already done by readSaveGameVersion
+ skipQDataStream(*m_Data, bytesToIgnore);
+
+ uint8_t version;
+ readQDataStream(*m_Data, version);
+ return version;
+
+ } else {
+ MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown "
+ "Compressed\" with your savefile attached");
+ return 0;
+ }
+}
+
+uint16_t GamebryoSaveGame::FileWrapper::readShort(int bytesToIgnore)
+{
+ if (m_CompressionType == 0) {
+ if (bytesToIgnore > 0) // Just to make certain
+ skip<char>(bytesToIgnore);
+ uint16_t size;
+ read(size);
+ return size;
+ } else if (m_CompressionType == 1 || m_CompressionType == 2) {
+ // decompression already done by readSaveGameVersion
+ skipQDataStream(*m_Data, bytesToIgnore);
+
+ uint16_t size;
+ readQDataStream(*m_Data, size);
+ return size;
+ } else {
+ MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown "
+ "Compressed\" with your savefile attached");
+ return 0;
+ }
+}
+
+uint32_t GamebryoSaveGame::FileWrapper::readInt(int bytesToIgnore)
+{
+ if (m_CompressionType == 0) {
+ if (bytesToIgnore > 0) // Just to make certain
+ skip<char>(bytesToIgnore);
+ uint32_t size;
+ read(size);
+ return size;
+ } else if (m_CompressionType == 1 || m_CompressionType == 2) {
+ // decompression already done by readSaveGameVersion
+ skipQDataStream(*m_Data, bytesToIgnore);
+
+ uint32_t size;
+ readQDataStream(*m_Data, size);
+ return size;
+ } else {
+ MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown "
+ "Compressed\" with your savefile attached");
+ return 0;
+ }
+}
+
+uint64_t GamebryoSaveGame::FileWrapper::readLong(int bytesToIgnore)
+{
+ if (m_CompressionType == 0) {
+ if (bytesToIgnore > 0) // Just to make certain
+ skip<char>(bytesToIgnore);
+ uint64_t size;
+ read(size);
+ return size;
+ } else if (m_CompressionType == 1 || m_CompressionType == 2) {
+ // decompression already done by readSaveGameVersion
+ skipQDataStream(*m_Data, bytesToIgnore);
+
+ uint64_t size;
+ readQDataStream(*m_Data, size);
+ return size;
+ } else {
+ MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown "
+ "Compressed\" with your savefile attached");
+ return 0;
+ }
+}
+
+float_t GamebryoSaveGame::FileWrapper::readFloat(int bytesToIgnore)
+{
+ if (m_CompressionType == 0) {
+ if (bytesToIgnore > 0) // Just to make certain
+ skip<char>(bytesToIgnore);
+ float_t value;
+ read(value);
+ return value;
+ } else if (m_CompressionType == 1 || m_CompressionType == 2) {
+ // decompression already done by readSaveGameVersion
+ skipQDataStream(*m_Data, bytesToIgnore);
+
+ float_t value;
+ readQDataStream(*m_Data, value);
+ return value;
+ } else {
+ MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown "
+ "Compressed\" with your savefile attached");
+ return 0;
+ }
+}
+
+QStringList GamebryoSaveGame::FileWrapper::readPlugins(int bytesToIgnore, int extraData,
+ const QStringList& corePlugins)
+{
+ if (m_CompressionType == 0) {
+ if (bytesToIgnore > 0) // Just to make certain
+ skip<char>(bytesToIgnore);
+ uint8_t count;
+ read(count);
+ return readPluginData(count, extraData, corePlugins);
+ } else if (m_CompressionType == 1 || m_CompressionType == 2) {
+ skipQDataStream(*m_Data, bytesToIgnore);
+ uint8_t count;
+ readQDataStream(*m_Data, count);
+ return readPluginData(count, extraData, corePlugins);
+ }
+ return {};
+}
+
+QStringList
+GamebryoSaveGame::FileWrapper::readLightPlugins(int bytesToIgnore, int extraData,
+ const QStringList& corePlugins)
+{
+ if (m_CompressionType == 0) {
+ if (bytesToIgnore > 0) // Just to make certain
+ skip<char>(bytesToIgnore);
+ uint16_t count;
+ read(count);
+ return readPluginData(count, extraData, corePlugins);
+ } else if (m_CompressionType == 1 || m_CompressionType == 2) {
+ skipQDataStream(*m_Data, bytesToIgnore);
+ uint16_t count;
+ readQDataStream(*m_Data, count);
+ return readPluginData(count, extraData, corePlugins);
+ }
+ return {};
+}
+
+QStringList
+GamebryoSaveGame::FileWrapper::readMediumPlugins(int bytesToIgnore, int extraData,
+ const QStringList& corePlugins)
+{
+ if (m_CompressionType != 1) {
+ return {};
+ } else {
+ skipQDataStream(*m_Data, bytesToIgnore);
+ uint32_t count;
+ readQDataStream(*m_Data, count);
+ return readPluginData(count, extraData, corePlugins);
+ }
+}
+
+QStringList GamebryoSaveGame::FileWrapper::readPluginData(uint32_t count, int extraData,
+ const QStringList corePlugins)
+{
+ QStringList plugins;
+ plugins.reserve(count);
+ if (m_CompressionType == 0) {
+ for (std::size_t i = 0; i < count; ++i) {
+ QString name;
+ read(name);
+ plugins.push_back(name);
+ }
+ } else {
+ for (std::size_t i = 0; i < count; ++i) {
+ QString name;
+ read(name);
+ plugins.push_back(name);
+ bool isCustomPlugin;
+ if (extraData) {
+ if (extraData > 1) {
+ readQDataStream(*m_Data, isCustomPlugin);
+ } else {
+ isCustomPlugin = !corePlugins.contains(name);
+ }
+ if (isCustomPlugin) {
+ QString creationName;
+ QString creationId;
+ uint16_t flagsSize;
+ uint8_t isCreation;
+ read(creationName);
+ read(creationId);
+ readQDataStream(*m_Data, flagsSize);
+ skipQDataStream(*m_Data, flagsSize);
+ readQDataStream(*m_Data, isCreation);
+ }
+ }
+ }
+ }
+ return plugins;
+}
+
+void GamebryoSaveGame::FileWrapper::close()
+{
+ m_File.close();
+}
diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegame.h b/libs/game_bethesda/src/gamebryo/gamebryosavegame.h
new file mode 100644
index 0000000..43dea11
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryosavegame.h
@@ -0,0 +1,256 @@
+#ifndef GAMEBRYOSAVEGAME_H
+#define GAMEBRYOSAVEGAME_H
+
+#include "isavegame.h"
+#include "memoizedlock.h"
+
+#include <QDateTime>
+#include <QFile>
+#include <QImage>
+#include <QString>
+#include <QStringList>
+
+#include <cstdint>
+#include <stddef.h>
+#include <stdexcept>
+
+#ifdef _WIN32
+struct _SYSTEMTIME;
+#else
+// Windows SYSTEMTIME struct - used in Bethesda savegame binary format
+struct _SYSTEMTIME {
+ uint16_t wYear;
+ uint16_t wMonth;
+ uint16_t wDayOfWeek;
+ uint16_t wDay;
+ uint16_t wHour;
+ uint16_t wMinute;
+ uint16_t wSecond;
+ uint16_t wMilliseconds;
+};
+#endif
+
+namespace MOBase
+{
+class IPluginGame;
+} // namespace MOBase
+
+class GameGamebryo;
+
+class GamebryoSaveGame : public MOBase::ISaveGame
+{
+public:
+ GamebryoSaveGame(QString const& file, GameGamebryo const* game,
+ bool const lightEnabled = false, bool const mediumEnabled = false);
+
+ virtual ~GamebryoSaveGame();
+
+public: // ISaveGame interface
+ virtual QString getFilepath() const override;
+ virtual QDateTime getCreationTime() const override;
+ virtual QString getName() const override;
+ virtual QString getSaveGroupIdentifier() const override;
+ virtual QStringList allFiles() const override;
+
+public:
+ bool hasScriptExtenderFile() const;
+
+ // Simple getters
+ virtual QString getPCName() const { return m_PCName; }
+ virtual unsigned short getPCLevel() const { return m_PCLevel; }
+ virtual QString getPCLocation() const { return m_PCLocation; }
+ virtual uint32_t getSaveNumber() const { return m_SaveNumber; }
+
+ QStringList const& getPlugins() const { return m_DataFields.value()->Plugins; }
+ QStringList const& getMediumPlugins() const
+ {
+ return m_DataFields.value()->MediumPlugins;
+ }
+ QStringList const& getLightPlugins() const
+ {
+ return m_DataFields.value()->LightPlugins;
+ }
+ QImage const& getScreenshot() const { return m_DataFields.value()->Screenshot; }
+
+ bool isMediumEnabled() const { return m_MediumEnabled; }
+
+ bool isLightEnabled() const { return m_LightEnabled; }
+
+ enum class StringType
+ {
+ TYPE_BZSTRING,
+ TYPE_BSTRING,
+ TYPE_WSTRING
+ };
+
+ enum class StringFormat
+ {
+ UTF8,
+ LOCAL8BIT
+ };
+
+protected:
+ friend class FileWrapper;
+
+ class FileWrapper
+ {
+ public:
+ /**
+ * @brief Construct the save file information.
+ *
+ * @param filepath The path to the save file.
+ * @params expected Expecte bytes at start of file.
+ *
+ **/
+ FileWrapper(QString const& filepath, QString const& expected);
+
+ /** Set this for save games that have a marker at the end of each
+ * field. Specifically fallout
+ **/
+ void setHasFieldMarkers(bool);
+
+ /** Set bz string mode (1 byte length, null terminated)
+ **/
+ void setPluginString(StringType);
+
+ /** Set string format (utf-8, windows local 8 bit strings)
+ **/
+ void setPluginStringFormat(StringFormat);
+
+ template <typename T>
+ void skip(int count = 1)
+ {
+ if (!m_File.seek(m_File.pos() + count * sizeof(T))) {
+ throw std::runtime_error("unexpected end of file");
+ }
+ }
+
+ template <typename T>
+ void read(T& value)
+ {
+ int read = m_File.read(reinterpret_cast<char*>(&value), sizeof(T));
+ if (read != sizeof(T)) {
+ throw std::runtime_error("unexpected end of file");
+ }
+ if (m_HasFieldMarkers) {
+ skip<char>();
+ }
+ }
+
+ void seek(unsigned long pos)
+ {
+ if (!m_File.seek(pos)) {
+ throw std::runtime_error("unexpected end of file");
+ }
+ }
+
+ void read(void* buff, std::size_t length);
+
+ /* Reads RGB image from save
+ * Assumes picture dimentions come immediately before the save
+ */
+ QImage readImage(int scale = 0, bool alpha = false);
+
+ /* Reads RGB image from save */
+ QImage readImage(uint32_t width, uint32_t height, int scale = 0,
+ bool alpha = false);
+
+ /* Sets the compression type. */
+ void setCompressionType(uint16_t type);
+
+ /* uncompress the begining of the compressed block */
+ bool openCompressedData(int bytesToIgnore = 0);
+
+ /* read the next compressed block */
+ bool readNextChunk();
+
+ /* frees the uncompressed block */
+ void closeCompressedData();
+
+ /* Read the save game version in the compressed block */
+ uint8_t readChar(int bytesToIgnore = 0);
+
+ uint16_t readShort(int bytesToIgnore = 0);
+
+ uint32_t readInt(int bytesToIgnore = 0);
+
+ uint64_t readLong(int bytesToIgnore = 0);
+
+ float_t readFloat(int bytesToIgnore = 0);
+
+ /* Read the plugin list */
+ QStringList readPlugins(int bytesToIgnore = 0, int extraData = 0,
+ const QStringList& corePlugins = {});
+
+ /* Read the light plugin list */
+ QStringList readLightPlugins(int bytesToIgnore = 0, int extraData = 0,
+ const QStringList& corePlugins = {});
+
+ /* Read the medium plugin list */
+ QStringList readMediumPlugins(int bytesToIgnore = 0, int extraData = 0,
+ const QStringList& corePlugins = {});
+
+ void close();
+
+ private:
+ QFile m_File;
+ uint64_t m_NextChunk;
+ uint64_t m_UncompressedSize;
+ bool m_HasFieldMarkers;
+ StringType m_PluginString;
+ StringFormat m_PluginStringFormat;
+ QDataStream* m_Data;
+ uint16_t m_CompressionType = 0;
+
+ private:
+ template <typename T>
+ void readQDataStream(QDataStream& data, T& value);
+
+ void readQDataStream(QDataStream& data, void* buff, std::size_t length);
+
+ void skipQDataStream(QDataStream& data, std::size_t length);
+
+ QStringList readPluginData(uint32_t count, int extraData,
+ const QStringList corePlugins);
+ };
+
+ void setCreationTime(_SYSTEMTIME const& time);
+
+ GameGamebryo const* m_Game;
+ bool m_MediumEnabled;
+ bool m_LightEnabled;
+
+ QString m_FileName;
+ QString m_PCName;
+ unsigned short m_PCLevel;
+ QString m_PCLocation;
+ uint32_t m_SaveNumber;
+ QDateTime m_CreationTime;
+
+ // Those three fields are usually much slower to fetch than
+ // the other, so we do not fetch them if not needed.
+ //
+ // This is virtual so child class can add fields if those are
+ // hard to access.
+ struct DataFields
+ {
+ QStringList Plugins;
+ QStringList LightPlugins;
+ QStringList MediumPlugins;
+ QImage Screenshot;
+
+ // We need this constructor.
+ DataFields() {}
+ virtual ~DataFields() {}
+ };
+ MOBase::MemoizedLocked<std::unique_ptr<DataFields>> m_DataFields;
+
+ // Fetch the field.
+ virtual std::unique_ptr<DataFields> fetchDataFields() const = 0;
+};
+
+// Explicit template specialization must be at namespace scope (GCC requirement)
+template <>
+void GamebryoSaveGame::FileWrapper::read<QString>(QString& value);
+
+#endif // GAMEBRYOSAVEGAME_H
diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.cpp b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.cpp
new file mode 100644
index 0000000..6d1289a
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.cpp
@@ -0,0 +1,100 @@
+#include "gamebryosavegameinfo.h"
+
+#include "gamebryosavegame.h"
+#include "gamebryosavegameinfowidget.h"
+#include "gamegamebryo.h"
+#include "imodinterface.h"
+#include "imoinfo.h"
+#include "iplugingame.h"
+#include "ipluginlist.h"
+
+#include <QDir>
+#include <QString>
+#include <QStringList>
+
+GamebryoSaveGameInfo::GamebryoSaveGameInfo(GameGamebryo const* game) : m_Game(game) {}
+
+GamebryoSaveGameInfo::~GamebryoSaveGameInfo() {}
+
+GamebryoSaveGameInfo::MissingAssets
+GamebryoSaveGameInfo::getMissingAssets(MOBase::ISaveGame const& save) const
+{
+ GamebryoSaveGame const& gamebryoSave = dynamic_cast<GamebryoSaveGame const&>(save);
+ MOBase::IOrganizer* organizerCore = m_Game->m_Organizer;
+
+ // collect the list of missing plugins
+ MissingAssets missingAssets;
+
+ for (QString const& pluginName : gamebryoSave.getPlugins()) {
+ switch (organizerCore->pluginList()->state(pluginName)) {
+ case MOBase::IPluginList::STATE_INACTIVE:
+ missingAssets[pluginName] =
+ ProvidingModules{organizerCore->pluginList()->origin(pluginName)};
+ break;
+ case MOBase::IPluginList::STATE_MISSING:
+ missingAssets[pluginName] = ProvidingModules();
+ break;
+ }
+ }
+
+ for (QString const& pluginName : gamebryoSave.getLightPlugins()) {
+ switch (organizerCore->pluginList()->state(pluginName)) {
+ case MOBase::IPluginList::STATE_INACTIVE:
+ missingAssets[pluginName] =
+ ProvidingModules{organizerCore->pluginList()->origin(pluginName)};
+ break;
+ case MOBase::IPluginList::STATE_MISSING:
+ missingAssets[pluginName] = ProvidingModules();
+ break;
+ }
+ }
+
+ // Find out any other mods that might contain the esp/esm
+ QStringList espFilter({"*.esp", "*.esl", "*.esm"});
+
+ QString dataDir(organizerCore->managedGame()->dataDirectory().absolutePath());
+
+ // Search normal mods. A note: This will also find mods in data.
+ for (QString const& mod : organizerCore->modList()->allModsByProfilePriority()) {
+ MOBase::IModInterface* modInfo = organizerCore->modList()->getMod(mod);
+ QStringList esps = QDir(modInfo->absolutePath()).entryList(espFilter);
+ for (QString const& esp : esps) {
+ MissingAssets::iterator iter = missingAssets.find(esp);
+ if (modInfo->absolutePath() == dataDir) {
+ // We have to prune esps that reside in the data directory, otherwise
+ // you get all the unmanaged mods listed as potential candidates for
+ // enabling
+ if (modInfo->name() != organizerCore->pluginList()->origin(esp)) {
+ continue;
+ }
+ }
+ if (iter != missingAssets.end()) {
+ if (!iter->contains(modInfo->name())) {
+ iter->push_back(modInfo->name());
+ }
+ }
+ }
+ }
+
+ // search in overwrite
+ {
+ QDir overwriteDir(organizerCore->overwritePath());
+ QStringList esps = overwriteDir.entryList(espFilter);
+ for (const QString& esp : esps) {
+ MissingAssets::iterator iter = missingAssets.find(esp);
+ if (iter != missingAssets.end()) {
+ if (!iter->contains("<overwrite>")) {
+ iter->push_back("<overwrite>");
+ }
+ }
+ }
+ }
+
+ return missingAssets;
+}
+
+MOBase::ISaveGameInfoWidget*
+GamebryoSaveGameInfo::getSaveGameWidget(QWidget* parent) const
+{
+ return new GamebryoSaveGameInfoWidget(this, parent);
+}
diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.h b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.h
new file mode 100644
index 0000000..e214293
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.h
@@ -0,0 +1,23 @@
+#ifndef GAMEBRYOSAVEGAMEINFO_H
+#define GAMEBRYOSAVEGAMEINFO_H
+
+#include "savegameinfo.h"
+
+class GameGamebryo;
+
+class GamebryoSaveGameInfo : public MOBase::SaveGameInfo
+{
+public:
+ GamebryoSaveGameInfo(GameGamebryo const* game);
+ ~GamebryoSaveGameInfo();
+
+ virtual MissingAssets getMissingAssets(MOBase::ISaveGame const& save) const override;
+
+ virtual MOBase::ISaveGameInfoWidget* getSaveGameWidget(QWidget*) const override;
+
+protected:
+ friend class GamebryoSaveGameInfoWidget;
+ GameGamebryo const* m_Game;
+};
+
+#endif // GAMEBRYOSAVEGAMEINFO_H
diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.cpp b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.cpp
new file mode 100644
index 0000000..8c95dad
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.cpp
@@ -0,0 +1,199 @@
+#include "gamebryosavegameinfowidget.h"
+#include "ui_gamebryosavegameinfowidget.h"
+
+#include "gamebryosavegame.h"
+#include "gamebryosavegameinfo.h"
+#include "gamegamebryo.h"
+#include "imoinfo.h"
+#include "ipluginlist.h"
+
+#include <QDate>
+#include <QDateTime>
+#include <QFont>
+#include <QFrame>
+#include <QLabel>
+#include <QLayout>
+#include <QLayoutItem>
+#include <QPixmap>
+#include <QString>
+#include <QStyle>
+#include <QTime>
+#include <QVBoxLayout>
+
+#include <Qt>
+#include <QtGlobal>
+
+#include <memory>
+
+GamebryoSaveGameInfoWidget::GamebryoSaveGameInfoWidget(GamebryoSaveGameInfo const* info,
+ QWidget* parent)
+ : MOBase::ISaveGameInfoWidget(parent), ui(new Ui::GamebryoSaveGameInfoWidget),
+ 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);
+}
+
+GamebryoSaveGameInfoWidget::~GamebryoSaveGameInfoWidget()
+{
+ delete ui;
+}
+
+void GamebryoSaveGameInfoWidget::setSave(MOBase::ISaveGame const& save)
+{
+ auto& gamebryoSave = dynamic_cast<GamebryoSaveGame const&>(save);
+ ui->saveNumLabel->setText(QString("%1").arg(gamebryoSave.getSaveNumber()));
+ ui->characterLabel->setText(gamebryoSave.getPCName());
+ ui->locationLabel->setText(gamebryoSave.getPCLocation());
+ ui->levelLabel->setText(QString("%1").arg(gamebryoSave.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 = gamebryoSave.getCreationTime().toLocalTime();
+ ui->dateLabel->setText(
+ QLocale::system().toString(t.date(), QLocale::FormatType::ShortFormat) + " " +
+ QLocale::system().toString(t.time()));
+ ui->screenshotLabel->setPixmap(QPixmap::fromImage(gamebryoSave.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();
+ if (gamebryoSave.hasScriptExtenderFile()) {
+ QLabel* scriptExtender = new QLabel(tr("Has Script Extender Data"));
+ QFont headerFont = scriptExtender->font();
+ headerFont.setBold(true);
+ layout->addWidget(scriptExtender);
+ }
+ 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 : gamebryoSave.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);
+ }
+ if (gamebryoSave.isMediumEnabled()) {
+ QLabel* headerEsh = new QLabel(tr("Missing ESHs"));
+ QFont headerEshFont = headerEsh->font();
+ QFont contentEshFont = headerEshFont;
+ headerEshFont.setItalic(true);
+ contentEshFont.setBold(true);
+ contentEshFont.setPointSize(7);
+ headerEsh->setFont(headerEshFont);
+ layout->addWidget(headerEsh);
+ int countEsh = 0;
+ for (QString const& pluginName : gamebryoSave.getMediumPlugins()) {
+ if (pluginList->state(pluginName) == MOBase::IPluginList::STATE_ACTIVE) {
+ continue;
+ }
+
+ ++countEsh;
+
+ if (countEsh > 7) {
+ break;
+ }
+
+ QLabel* pluginLabel = new QLabel(pluginName);
+ pluginLabel->setIndent(10);
+ pluginLabel->setFont(contentFont);
+ layout->addWidget(pluginLabel);
+ }
+ if (countEsh > 7) {
+ QLabel* dotDotLabel = new QLabel("...");
+ dotDotLabel->setIndent(10);
+ dotDotLabel->setFont(contentFont);
+ layout->addWidget(dotDotLabel);
+ }
+ if (countEsh == 0) {
+ QLabel* dotDotLabel = new QLabel(tr("None"));
+ dotDotLabel->setIndent(10);
+ dotDotLabel->setFont(contentFont);
+ layout->addWidget(dotDotLabel);
+ }
+ }
+ if (gamebryoSave.isLightEnabled()) {
+ QLabel* headerEsl = new QLabel(tr("Missing ESLs"));
+ QFont headerEslFont = headerEsl->font();
+ QFont contentEslFont = headerEslFont;
+ headerEslFont.setItalic(true);
+ contentEslFont.setBold(true);
+ contentEslFont.setPointSize(7);
+ headerEsl->setFont(headerEslFont);
+ layout->addWidget(headerEsl);
+ int countEsl = 0;
+ for (QString const& pluginName : gamebryoSave.getLightPlugins()) {
+ if (pluginList->state(pluginName) == MOBase::IPluginList::STATE_ACTIVE) {
+ continue;
+ }
+
+ ++countEsl;
+
+ if (countEsl > 7) {
+ break;
+ }
+
+ QLabel* pluginLabel = new QLabel(pluginName);
+ pluginLabel->setIndent(10);
+ pluginLabel->setFont(contentFont);
+ layout->addWidget(pluginLabel);
+ }
+ if (countEsl > 7) {
+ QLabel* dotDotLabel = new QLabel("...");
+ dotDotLabel->setIndent(10);
+ dotDotLabel->setFont(contentFont);
+ layout->addWidget(dotDotLabel);
+ }
+ if (countEsl == 0) {
+ QLabel* dotDotLabel = new QLabel(tr("None"));
+ dotDotLabel->setIndent(10);
+ dotDotLabel->setFont(contentFont);
+ layout->addWidget(dotDotLabel);
+ }
+ }
+}
diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.h b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.h
new file mode 100644
index 0000000..6642863
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.h
@@ -0,0 +1,30 @@
+#ifndef GAMEBRYOSAVEGAMEINFOWIDGET_H
+#define GAMEBRYOSAVEGAMEINFOWIDGET_H
+
+#include "isavegameinfowidget.h"
+
+#include <QObject>
+
+class GamebryoSaveGameInfo;
+
+namespace Ui
+{
+class GamebryoSaveGameInfoWidget;
+}
+
+class GamebryoSaveGameInfoWidget : public MOBase::ISaveGameInfoWidget
+{
+ Q_OBJECT
+
+public:
+ GamebryoSaveGameInfoWidget(GamebryoSaveGameInfo const* info, QWidget* parent);
+ ~GamebryoSaveGameInfoWidget();
+
+ virtual void setSave(MOBase::ISaveGame const&) override;
+
+private:
+ Ui::GamebryoSaveGameInfoWidget* ui;
+ GamebryoSaveGameInfo const* m_Info;
+};
+
+#endif // GAMEBRYOSAVEGAMEINFOWIDGET_H
diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.ui b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.ui
new file mode 100644
index 0000000..ea45d10
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.ui
@@ -0,0 +1,209 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>GamebryoSaveGameInfoWidget</class>
+ <widget class="QWidget" name="GamebryoSaveGameInfoWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>300</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string/>
+ </property>
+ <property name="toolTip">
+ <string notr="true"/>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="fieldGrowthPolicy">
+ <enum>QFormLayout::AllNonFixedFieldsGrow</enum>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Save #</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Character</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Level</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Location</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0">
+ <widget class="QLabel" name="label_5">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Date</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="saveNumLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="characterLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="levelLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLabel" name="locationLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="1">
+ <widget class="QLabel" name="dateLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QFrame" name="gameFrame">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="screenshotLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/libs/game_bethesda/src/gamebryo/gamebryoscriptextender.cpp b/libs/game_bethesda/src/gamebryo/gamebryoscriptextender.cpp
new file mode 100644
index 0000000..e3f709a
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryoscriptextender.cpp
@@ -0,0 +1,46 @@
+#include "gamebryoscriptextender.h"
+
+#include "gamegamebryo.h"
+
+#include "utility.h"
+
+#include <QDir>
+#include <QString>
+
+GamebryoScriptExtender::GamebryoScriptExtender(const GameGamebryo* game) : m_Game(game)
+{}
+
+GamebryoScriptExtender::~GamebryoScriptExtender() {}
+
+QString GamebryoScriptExtender::loaderName() const
+{
+ return BinaryName();
+}
+
+QString GamebryoScriptExtender::loaderPath() const
+{
+ return m_Game->gameDirectory().absoluteFilePath(loaderName());
+}
+
+QString GamebryoScriptExtender::savegameExtension() const
+{
+ return m_Game->savegameSEExtension();
+}
+
+bool GamebryoScriptExtender::isInstalled() const
+{
+ // A note: It is possibly also OK if xxse_steam_loader.dll exists, but it's
+ // not clear why that would exist and the exe not if you'd installed it per
+ // instructions, and it'd mess up NCC installs a treat.
+ return m_Game->gameDirectory().exists(loaderName());
+}
+
+QString GamebryoScriptExtender::getExtenderVersion() const
+{
+ return MOBase::getFileVersion(m_Game->gameDirectory().absoluteFilePath(loaderName()));
+}
+
+WORD GamebryoScriptExtender::getArch() const
+{
+ return m_Game->getArch(loaderName());
+}
diff --git a/libs/game_bethesda/src/gamebryo/gamebryoscriptextender.h b/libs/game_bethesda/src/gamebryo/gamebryoscriptextender.h
new file mode 100644
index 0000000..7ab4f1d
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryoscriptextender.h
@@ -0,0 +1,31 @@
+#ifndef GAMEBRYOSCRIPTEXTENDER_H
+#define GAMEBRYOSCRIPTEXTENDER_H
+
+#include "scriptextender.h"
+
+class GameGamebryo;
+
+class GamebryoScriptExtender : public MOBase::ScriptExtender
+{
+public:
+ GamebryoScriptExtender(GameGamebryo const* game);
+
+ virtual ~GamebryoScriptExtender();
+
+ virtual QString loaderName() const override;
+
+ virtual QString loaderPath() const override;
+
+ virtual QString savegameExtension() const override;
+
+ virtual bool isInstalled() const override;
+
+ virtual QString getExtenderVersion() const override;
+
+ virtual WORD getArch() const override;
+
+protected:
+ GameGamebryo const* const m_Game;
+};
+
+#endif // GAMEBRYOSCRIPTEXTENDER_H
diff --git a/libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.cpp b/libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.cpp
new file mode 100644
index 0000000..e5c7608
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.cpp
@@ -0,0 +1,51 @@
+#include "gamebryounmanagedmods.h"
+#include "gamegamebryo.h"
+#include <pluginsetting.h>
+
+GamebryoUnmangedMods::GamebryoUnmangedMods(const GameGamebryo* game) : m_Game(game) {}
+
+GamebryoUnmangedMods::~GamebryoUnmangedMods() {}
+
+QStringList GamebryoUnmangedMods::mods(bool onlyOfficial) const
+{
+ QStringList result;
+
+ QStringList dlcPlugins = m_Game->DLCPlugins();
+ QStringList mainPlugins = m_Game->primaryPlugins();
+
+ QDir dataDir(m_Game->dataDirectory());
+ for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) {
+ if (!mainPlugins.contains(fileName, Qt::CaseInsensitive) &&
+ (!onlyOfficial || dlcPlugins.contains(fileName, Qt::CaseInsensitive))) {
+ result.append(fileName.chopped(4)); // trims the extension off
+ }
+ }
+
+ return result;
+}
+
+QString GamebryoUnmangedMods::displayName(const QString& modName) const
+{
+ return modName;
+}
+
+QFileInfo GamebryoUnmangedMods::referenceFile(const QString& modName) const
+{
+ QFileInfoList files =
+ m_Game->dataDirectory().entryInfoList(QStringList() << modName + ".es*");
+ if (files.size() > 0) {
+ return files.at(0);
+ } else {
+ return QFileInfo();
+ }
+}
+
+QStringList GamebryoUnmangedMods::secondaryFiles(const QString& modName) const
+{
+ QStringList archives;
+ QDir dataDir = m_Game->dataDirectory();
+ for (const QString& archiveName : dataDir.entryList({modName + "*.bsa"})) {
+ archives.append(dataDir.absoluteFilePath(archiveName));
+ }
+ return archives;
+}
diff --git a/libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.h b/libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.h
new file mode 100644
index 0000000..38e2ba5
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.h
@@ -0,0 +1,26 @@
+#ifndef GAMEBRYOUNMANAGEDMODS_H
+#define GAMEBRYOUNMANAGEDMODS_H
+
+#include <unmanagedmods.h>
+
+class GameGamebryo;
+
+class GamebryoUnmangedMods : public MOBase::UnmanagedMods
+{
+public:
+ GamebryoUnmangedMods(const GameGamebryo* game);
+ ~GamebryoUnmangedMods();
+
+ virtual QStringList mods(bool onlyOfficial) const override;
+ virtual QString displayName(const QString& modName) const override;
+ virtual QFileInfo referenceFile(const QString& modName) const override;
+ virtual QStringList secondaryFiles(const QString& modName) const override;
+
+protected:
+ const GameGamebryo* game() const { return m_Game; }
+
+private:
+ const GameGamebryo* m_Game;
+};
+
+#endif // GAMEBRYOUNMANAGEDMODS_H
diff --git a/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp
new file mode 100644
index 0000000..ac9fe22
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp
@@ -0,0 +1,823 @@
+#include "gamegamebryo.h"
+
+#include "bsainvalidation.h"
+#include "dataarchives.h"
+#include "gamebryomoddatacontent.h"
+#include "gamebryosavegame.h"
+#include "gameplugins.h"
+#include "iprofile.h"
+#include "log.h"
+#include "registry.h"
+#include "savegameinfo.h"
+#include "scopeguard.h"
+#include "scriptextender.h"
+#include "utility.h"
+#include "vdf_parser.h"
+
+#include <QDir>
+#include <QDirIterator>
+#include <QFile>
+#include <QFileInfo>
+#include <QIcon>
+#include <QJsonDocument>
+#include <QJsonValue>
+#include <QSettings>
+#include <QStandardPaths>
+#include <QTextStream>
+
+#include <QtDebug>
+#include <QtGlobal>
+
+#ifdef _WIN32
+#include <Knownfolders.h>
+#include <Shlobj.h>
+#include <Windows.h>
+#include <winreg.h>
+#include <winver.h>
+#endif
+
+#ifdef HAS_NAK_FFI
+#include <nak_ffi.h>
+#endif
+
+#include <optional>
+#include <string>
+#include <vector>
+
+// Case-insensitive file path resolution for Linux.
+// On Windows (case-insensitive FS), just returns the input path.
+// On Linux, if the exact path doesn't exist, searches the parent directory
+// for a file matching the name case-insensitively.
+static QString resolveFileCaseInsensitive(const QString& filePath)
+{
+#ifdef _WIN32
+ return filePath;
+#else
+ if (QFileInfo::exists(filePath)) {
+ return filePath;
+ }
+
+ QFileInfo info(filePath);
+ QDir dir(info.path());
+ if (!dir.exists()) {
+ return filePath;
+ }
+
+ const QString target = info.fileName();
+ const QStringList entries =
+ dir.entryList(QDir::Files | QDir::Hidden | QDir::System);
+ for (const QString& entry : entries) {
+ if (entry.compare(target, Qt::CaseInsensitive) == 0) {
+ return dir.absoluteFilePath(entry);
+ }
+ }
+
+ return filePath;
+#endif
+}
+
+GameGamebryo::GameGamebryo() {}
+
+void GameGamebryo::detectGame()
+{
+ m_GamePath = identifyGamePath();
+ m_MyGamesPath = determineMyGamesPath(gameName());
+}
+
+bool GameGamebryo::init(MOBase::IOrganizer* moInfo)
+{
+ m_Organizer = moInfo;
+ m_Organizer->onAboutToRun([this](const auto& binary) {
+ return prepareIni(binary);
+ });
+ return true;
+}
+
+bool GameGamebryo::isInstalled() const
+{
+ return !m_GamePath.isEmpty();
+}
+
+QIcon GameGamebryo::gameIcon() const
+{
+ return MOBase::iconForExecutable(gameDirectory().absoluteFilePath(binaryName()));
+}
+
+QDir GameGamebryo::gameDirectory() const
+{
+ return QDir(m_GamePath);
+}
+
+QDir GameGamebryo::dataDirectory() const
+{
+ return gameDirectory().absoluteFilePath("Data");
+}
+
+void GameGamebryo::setGamePath(const QString& path)
+{
+ m_GamePath = path;
+}
+
+QDir GameGamebryo::documentsDirectory() const
+{
+ return m_MyGamesPath;
+}
+
+QDir GameGamebryo::savesDirectory() const
+{
+ return QDir(myGamesPath() + "/Saves");
+}
+
+std::vector<std::shared_ptr<const MOBase::ISaveGame>>
+GameGamebryo::listSaves(QDir folder) const
+{
+ QStringList filters;
+ filters << QString("*.") + savegameExtension();
+
+ std::vector<std::shared_ptr<const MOBase::ISaveGame>> saves;
+ for (auto info : folder.entryInfoList(filters, QDir::Files)) {
+ try {
+ saves.push_back(makeSaveGame(info.filePath()));
+ } catch (std::exception& e) {
+ MOBase::log::error("{}", e.what());
+ continue;
+ }
+ }
+
+ return saves;
+}
+
+void GameGamebryo::setGameVariant(const QString& variant)
+{
+ m_GameVariant = variant;
+}
+
+QString GameGamebryo::binaryName() const
+{
+ return gameShortName() + ".exe";
+}
+
+MOBase::IPluginGame::LoadOrderMechanism GameGamebryo::loadOrderMechanism() const
+{
+ return LoadOrderMechanism::FileTime;
+}
+
+MOBase::IPluginGame::SortMechanism GameGamebryo::sortMechanism() const
+{
+ return SortMechanism::LOOT;
+}
+
+bool GameGamebryo::looksValid(QDir const& path) const
+{
+ // Check for <prog>.exe for now.
+ return path.exists(binaryName());
+}
+
+QString GameGamebryo::gameVersion() const
+{
+ // We try the file version, but if it looks invalid (starts with the fallback
+ // version), we look the product version instead. If the product version is
+ // not empty, we use it.
+ QString binaryAbsPath = gameDirectory().absoluteFilePath(binaryName());
+ QString version = MOBase::getFileVersion(binaryAbsPath);
+ if (version.startsWith(FALLBACK_GAME_VERSION)) {
+ QString pversion = MOBase::getProductVersion(binaryAbsPath);
+ if (!pversion.isEmpty()) {
+ version = pversion;
+ }
+ }
+ return version;
+}
+
+QString GameGamebryo::getLauncherName() const
+{
+ return gameShortName() + "Launcher.exe";
+}
+
+WORD GameGamebryo::getArch(QString const& program) const
+{
+#ifdef _WIN32
+ WORD arch = 0;
+ // This *really* needs to be factored out
+ std::wstring app_name =
+ L"\\\\?\\" +
+ QDir::toNativeSeparators(this->gameDirectory().absoluteFilePath(program))
+ .toStdWString();
+
+ WIN32_FIND_DATAW FindFileData;
+ HANDLE hFind = ::FindFirstFileW(app_name.c_str(), &FindFileData);
+
+ // exit if the binary was not found
+ if (hFind == INVALID_HANDLE_VALUE)
+ return arch;
+
+ HANDLE hFile = INVALID_HANDLE_VALUE;
+ HANDLE hMapping = INVALID_HANDLE_VALUE;
+ LPVOID addrHeader = nullptr;
+ PIMAGE_NT_HEADERS peHdr = nullptr;
+
+ hFile = CreateFileW(app_name.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL,
+ OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL);
+ if (hFile == INVALID_HANDLE_VALUE)
+ goto cleanup;
+
+ hMapping = CreateFileMappingW(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0,
+ program.toStdWString().c_str());
+ if (hMapping == INVALID_HANDLE_VALUE)
+ goto cleanup;
+
+ addrHeader = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
+ if (addrHeader == NULL)
+ goto cleanup; // couldn't memory map the file
+
+ peHdr = ImageNtHeader(addrHeader);
+ if (peHdr == NULL)
+ goto cleanup; // couldn't read the header
+
+ arch = peHdr->FileHeader.Machine;
+
+cleanup: // release all of our handles
+ FindClose(hFind);
+ if (hFile != INVALID_HANDLE_VALUE)
+ CloseHandle(hFile);
+ if (hMapping != INVALID_HANDLE_VALUE)
+ CloseHandle(hMapping);
+ return arch;
+#else
+ Q_UNUSED(program);
+ return 0;
+#endif
+}
+
+QFileInfo GameGamebryo::findInGameFolder(const QString& relativePath) const
+{
+ return QFileInfo(m_GamePath + "/" + relativePath);
+}
+
+QString GameGamebryo::identifyGamePath() const
+{
+#ifdef HAS_NAK_FFI
+ // Use NaK to detect game installations on Linux
+ NakGameList gameList = nak_detect_all_games();
+ ON_BLOCK_EXIT([&]() {
+ nak_game_list_free(gameList);
+ });
+
+ QString shortName = gameShortName();
+ QString fullName = gameName();
+
+ auto tokensMatch = [](const QString& detected, const QString& candidate) {
+ const QString detectedLower = detected.toLower();
+ const QStringList tokens =
+ candidate.toLower().split(QRegularExpression("[^a-z0-9]+"),
+ Qt::SkipEmptyParts);
+ bool anyToken = false;
+ for (const QString& token : tokens) {
+ if (token.size() < 3) {
+ continue;
+ }
+ anyToken = true;
+ if (!detectedLower.contains(token)) {
+ return false;
+ }
+ }
+ return anyToken;
+ };
+
+ for (size_t i = 0; i < gameList.count; ++i) {
+ const NakGame& game = gameList.games[i];
+ QString detectedName = QString::fromUtf8(game.name);
+ QString detectedPath = QString::fromUtf8(game.install_path);
+ if (detectedName.compare(fullName, Qt::CaseInsensitive) == 0 ||
+ detectedName.compare(shortName, Qt::CaseInsensitive) == 0 ||
+ detectedName.contains(fullName, Qt::CaseInsensitive) ||
+ detectedName.contains(shortName, Qt::CaseInsensitive) ||
+ tokensMatch(detectedName, fullName) || tokensMatch(detectedName, shortName)) {
+ if (looksValid(QDir(detectedPath))) {
+ return detectedPath;
+ }
+ }
+ }
+ return {};
+#elif defined(_WIN32)
+ QString path = "Software\\Bethesda Softworks\\" + gameShortName();
+ return findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(),
+ L"Installed Path");
+#else
+ return {};
+#endif
+}
+
+bool GameGamebryo::prepareIni(const QString&)
+{
+ const auto profile = m_Organizer->profile();
+
+ QString basePath = profile->localSettingsEnabled()
+ ? profile->absolutePath()
+ : documentsDirectory().absolutePath();
+
+ // Ensure all INI files exist with adequate content before writing settings.
+ // On Linux, the game launcher often doesn't work properly and can't create
+ // the INI files. If an INI is missing or is a stub (< 200 bytes, meaning
+ // only MO2-written keys exist), seed it from the game's default INI template.
+ ensureIniFilesExist(basePath);
+
+ if (!iniFiles().isEmpty()) {
+ // Resolve case-insensitively (e.g., fallout.ini vs Fallout.ini on Linux)
+ QString profileIni =
+ resolveFileCaseInsensitive(basePath + "/" + iniFiles()[0]);
+
+ QString setting = readIniValue(profileIni, "Launcher", "bEnableFileSelection", "0");
+ if (setting.toLong() != 1) {
+ MOBase::WriteRegistryValue("Launcher", "bEnableFileSelection", "1", profileIni);
+ }
+ }
+
+ return true;
+}
+
+void GameGamebryo::ensureIniFilesExist(const QString& basePath)
+{
+ // Make sure the target directory exists
+ QDir baseDir(basePath);
+ if (!baseDir.exists()) {
+ baseDir.mkpath(".");
+ }
+
+ for (const QString& iniFile : iniFiles()) {
+ QString targetPath = basePath + "/" + iniFile;
+ QFileInfo targetInfo(targetPath);
+
+ // Check if the target file already has adequate content
+ // (> 200 bytes = more than just MO2's BSA invalidation stub)
+ if (targetInfo.exists() && targetInfo.size() > 200) {
+ continue;
+ }
+
+#ifndef _WIN32
+ // On Linux, search for a differently-cased version of this INI file
+ // (e.g., FalloutPrefs.ini when we expect falloutprefs.ini).
+ // We can't use resolveFileCaseInsensitive here because the exact-case
+ // file might exist as an empty stub while the real one has different case.
+ QString caseMismatchPath;
+ {
+ const QString target = targetInfo.fileName();
+ const QStringList entries =
+ baseDir.entryList(QDir::Files | QDir::Hidden | QDir::System);
+ for (const QString& entry : entries) {
+ if (entry != target &&
+ entry.compare(target, Qt::CaseInsensitive) == 0) {
+ caseMismatchPath = baseDir.absoluteFilePath(entry);
+ break;
+ }
+ }
+ }
+
+ // If a differently-cased version exists with adequate content,
+ // replace the stub (if any) with a symlink to the real file.
+ if (!caseMismatchPath.isEmpty()) {
+ QFileInfo altInfo(caseMismatchPath);
+ if (altInfo.exists() && altInfo.size() > 200) {
+ if (targetInfo.exists()) {
+ QFile::remove(targetPath);
+ }
+ QFile::link(altInfo.fileName(), targetPath);
+ MOBase::log::info("Created case-matching symlink '{}' -> '{}'",
+ iniFile, altInfo.fileName());
+ continue;
+ }
+ }
+#endif
+
+ // The INI doesn't exist or is a stub, and no adequate differently-cased
+ // version was found. Try to seed from the game's default INI template
+ // (e.g., fallout_default.ini, oblivion_default.ini).
+ QString baseName = QFileInfo(iniFile).completeBaseName(); // "fallout"
+ QString defaultIniName = baseName + "_default.ini";
+ QString defaultIniPath =
+ resolveFileCaseInsensitive(gameDirectory().absoluteFilePath(defaultIniName));
+
+ if (QFileInfo::exists(defaultIniPath)) {
+ // Remove the stub file if it exists so we can replace it
+ if (targetInfo.exists()) {
+ QFile::remove(targetPath);
+ }
+ if (QFile::copy(defaultIniPath, targetPath)) {
+ // Make the copy writable
+ QFile::setPermissions(
+ targetPath,
+ QFile::permissions(targetPath) | QFile::WriteUser | QFile::WriteOwner);
+ MOBase::log::info("Seeded '{}' from default INI '{}'", iniFile, defaultIniPath);
+ } else {
+ MOBase::log::warn("Failed to copy default INI '{}' -> '{}'",
+ defaultIniPath, targetPath);
+ }
+ }
+ }
+}
+
+QString GameGamebryo::readIniValue(const QString& iniFile, const QString& section,
+ const QString& key, const QString& defaultValue)
+{
+ // Read INI values directly without QSettings to avoid QSettings
+ // misinterpreting backslashes as line continuations.
+ QFile file(iniFile);
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ return defaultValue;
+ }
+
+ QString sectionHeader = "[" + section + "]";
+ QTextStream in(&file);
+ bool inSection = false;
+
+ while (!in.atEnd()) {
+ QString line = in.readLine().trimmed();
+
+ if (line.startsWith('[') && line.endsWith(']')) {
+ if (inSection) {
+ break; // Left the target section without finding key
+ }
+ if (line.compare(sectionHeader, Qt::CaseInsensitive) == 0) {
+ inSection = true;
+ }
+ continue;
+ }
+
+ if (inSection && !line.isEmpty() && !line.startsWith(';') && !line.startsWith('#')) {
+ int eqPos = line.indexOf('=');
+ if (eqPos > 0) {
+ QString existingKey = line.left(eqPos).trimmed();
+ if (existingKey.compare(key, Qt::CaseInsensitive) == 0) {
+ return line.mid(eqPos + 1).trimmed();
+ }
+ }
+ }
+ }
+
+ return defaultValue;
+}
+
+QString GameGamebryo::selectedVariant() const
+{
+ return m_GameVariant;
+}
+
+QString GameGamebryo::myGamesPath() const
+{
+ return m_MyGamesPath;
+}
+
+/*static*/ QString GameGamebryo::getLootPath()
+{
+#ifdef _WIN32
+ return findInRegistry(HKEY_LOCAL_MACHINE, L"Software\\LOOT", L"Installed Path") +
+ "/Loot.exe";
+#else
+ // On Linux, look for loot in common locations
+ QString flatpakPath = QDir::homePath() + "/.var/app/io.github.loot.loot/data/LOOT/Loot";
+ if (QFileInfo(flatpakPath).exists())
+ return flatpakPath;
+ // Try system PATH
+ QString systemLoot = QStandardPaths::findExecutable("loot");
+ if (!systemLoot.isEmpty())
+ return systemLoot;
+ return "loot";
+#endif
+}
+
+QString GameGamebryo::localAppFolder()
+{
+#ifdef _WIN32
+ QString result = getKnownFolderPath(FOLDERID_LocalAppData, false);
+ if (result.isEmpty()) {
+ // fallback: try the registry
+ result = getSpecialPath("Local AppData");
+ }
+ return result;
+#else
+ return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
+#endif
+}
+
+void GameGamebryo::copyToProfile(QString const& sourcePath,
+ QDir const& destinationDirectory,
+ QString const& sourceFileName)
+{
+ copyToProfile(sourcePath, destinationDirectory, sourceFileName, sourceFileName);
+}
+
+void GameGamebryo::copyToProfile(QString const& sourcePath,
+ QDir const& destinationDirectory,
+ QString const& sourceFileName,
+ QString const& destinationFileName)
+{
+ QString filePath = destinationDirectory.absoluteFilePath(destinationFileName);
+ if (!QFileInfo(filePath).exists()) {
+ QString sourceFile = sourcePath + "/" + sourceFileName;
+ // On Linux, try case-insensitive match if the exact source doesn't exist
+ sourceFile = resolveFileCaseInsensitive(sourceFile);
+ if (!MOBase::shellCopy(sourceFile, filePath)) {
+ // if copy file fails, create the file empty
+ QFile(filePath).open(QIODevice::WriteOnly);
+ }
+ }
+}
+
+MappingType GameGamebryo::mappings() const
+{
+ MappingType result;
+
+ for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) {
+ result.push_back({m_Organizer->profilePath() + "/" + profileFile,
+ localAppFolder() + "/" + gameShortName() + "/" + profileFile,
+ false});
+ }
+
+ return result;
+}
+
+#ifdef _WIN32
+std::unique_ptr<BYTE[]> GameGamebryo::getRegValue(HKEY key, LPCWSTR path, LPCWSTR value,
+ DWORD flags, LPDWORD type)
+{
+ DWORD size = 0;
+ HKEY subKey;
+ LONG res = ::RegOpenKeyExW(key, path, 0, KEY_QUERY_VALUE | KEY_WOW64_32KEY, &subKey);
+ if (res != ERROR_SUCCESS) {
+ res = ::RegOpenKeyExW(key, path, 0, KEY_QUERY_VALUE | KEY_WOW64_64KEY, &subKey);
+ if (res != ERROR_SUCCESS)
+ return std::unique_ptr<BYTE[]>();
+ }
+ res = ::RegGetValueW(subKey, L"", value, flags, type, nullptr, &size);
+ if (res == ERROR_FILE_NOT_FOUND || res == ERROR_UNSUPPORTED_TYPE) {
+ return std::unique_ptr<BYTE[]>();
+ }
+ if (res != ERROR_SUCCESS && res != ERROR_MORE_DATA) {
+ throw MOBase::MyException(
+ QObject::tr("failed to query registry path (preflight): %1").arg(res, 0, 16));
+ }
+
+ std::unique_ptr<BYTE[]> result(new BYTE[size]);
+ res = ::RegGetValueW(subKey, L"", value, flags, type, result.get(), &size);
+
+ if (res != ERROR_SUCCESS) {
+ throw MOBase::MyException(
+ QObject::tr("failed to query registry path (read): %1").arg(res, 0, 16));
+ }
+
+ return result;
+}
+
+QString GameGamebryo::findInRegistry(HKEY baseKey, LPCWSTR path, LPCWSTR value)
+{
+ std::unique_ptr<BYTE[]> buffer =
+ getRegValue(baseKey, path, value, RRF_RT_REG_SZ | RRF_NOEXPAND);
+
+ return QString::fromUtf16(reinterpret_cast<const char16_t*>(buffer.get()));
+}
+
+QString GameGamebryo::getKnownFolderPath(REFKNOWNFOLDERID folderId, bool useDefault)
+{
+ PWSTR path = nullptr;
+ ON_BLOCK_EXIT([&]() {
+ if (path != nullptr)
+ ::CoTaskMemFree(path);
+ });
+
+ if (::SHGetKnownFolderPath(folderId, useDefault ? KF_FLAG_DEFAULT_PATH : 0, NULL,
+ &path) == S_OK) {
+ return QDir::fromNativeSeparators(QString::fromWCharArray(path));
+ } else {
+ return QString();
+ }
+}
+
+QString GameGamebryo::getSpecialPath(const QString& name)
+{
+ QString base = findInRegistry(
+ HKEY_CURRENT_USER,
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders",
+ name.toStdWString().c_str());
+
+ WCHAR temp[MAX_PATH];
+ if (::ExpandEnvironmentStringsW(base.toStdWString().c_str(), temp, MAX_PATH) != 0) {
+ return QString::fromWCharArray(temp);
+ } else {
+ return base;
+ }
+}
+#endif // _WIN32
+
+QString GameGamebryo::determineMyGamesPath(const QString& gameName)
+{
+ const QString pattern = "%1/My Games/" + gameName;
+
+ auto tryDir = [&](const QString& dir) -> std::optional<QString> {
+ if (dir.isEmpty()) {
+ return {};
+ }
+
+ const auto path = pattern.arg(dir);
+ if (!QFileInfo(path).exists()) {
+ return {};
+ }
+
+ return path;
+ };
+
+#ifdef _WIN32
+ // a) this is the way it should work. get the configured My Documents directory
+ if (auto d = tryDir(getKnownFolderPath(FOLDERID_Documents, false))) {
+ return *d;
+ }
+
+ // b) if there is no <game> directory there, look in the default directory
+ if (auto d = tryDir(getKnownFolderPath(FOLDERID_Documents, true))) {
+ return *d;
+ }
+
+ // c) finally, look in the registry. This is discouraged
+ if (auto d = tryDir(getSpecialPath("Personal"))) {
+ return *d;
+ }
+#else
+ // On Linux, My Games is inside the Wine prefix's Documents folder.
+ // Check common Wine prefix locations for steamuser Documents.
+ QStringList prefixDocPaths;
+
+ // First check the configured prefix from MO2 settings (most reliable).
+ const QString configuredPrefix =
+ QSettings().value("fluorine/prefix_path").toString().trimmed();
+ if (!configuredPrefix.isEmpty()) {
+ const QString configuredDocs =
+ QDir(configuredPrefix).filePath("drive_c/users/steamuser/Documents");
+ prefixDocPaths.append(configuredDocs);
+ }
+
+ // Standard Steam Proton prefix paths
+ QString steamRoot = QDir::homePath() + "/.steam/steam/steamapps/compatdata";
+ QDir compatDir(steamRoot);
+ if (compatDir.exists()) {
+ QStringList appIds = compatDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
+ for (const QString& appId : appIds) {
+ prefixDocPaths.append(steamRoot + "/" + appId +
+ "/pfx/drive_c/users/steamuser/Documents");
+ }
+ }
+
+ // Also check XDG Documents (for native games or manual setups)
+ prefixDocPaths.append(
+ QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
+
+ for (const QString& docPath : prefixDocPaths) {
+ if (auto d = tryDir(docPath)) {
+ MOBase::log::debug("determineMyGamesPath: found '{}' for game '{}'", *d, gameName);
+ return *d;
+ }
+ }
+
+ // If no existing directory was found, try to create it in the configured prefix
+ // so that the game launcher can populate it on first run.
+ if (!configuredPrefix.isEmpty()) {
+ const QString configuredDocs =
+ QDir(configuredPrefix).filePath("drive_c/users/steamuser/Documents");
+ const QString newPath = pattern.arg(configuredDocs);
+ if (QDir().mkpath(newPath)) {
+ MOBase::log::info("determineMyGamesPath: created '{}' for game '{}'", newPath,
+ gameName);
+ return newPath;
+ }
+ }
+
+ MOBase::log::warn("determineMyGamesPath: could not find My Games path for '{}'",
+ gameName);
+#endif
+
+ return {};
+}
+
+QString GameGamebryo::parseEpicGamesLocation(const QStringList& manifests)
+{
+#ifdef _WIN32
+ // Use the registry entry to find the EGL Data dir first, just in case something
+ // changes
+ QString manifestDir = findInRegistry(
+ HKEY_LOCAL_MACHINE, L"Software\\Epic Games\\EpicGamesLauncher", L"AppDataPath");
+ if (manifestDir.isEmpty())
+ manifestDir = getKnownFolderPath(FOLDERID_ProgramData, false) +
+ "\\Epic\\EpicGamesLauncher\\Data\\";
+ manifestDir += "Manifests";
+#else
+ // Epic Games on Linux (via Heroic or Legendary)
+ QString manifestDir = QDir::homePath() +
+ "/.config/heroic/store_cache/egl_manifests";
+ if (!QDir(manifestDir).exists()) {
+ // Try Legendary
+ manifestDir = QDir::homePath() + "/.config/legendary/installed.json";
+ // Legendary uses a different format - skip for now
+ return "";
+ }
+#endif
+
+ QDir epicManifests(manifestDir, "*.item",
+ QDir::SortFlags(QDir::Name | QDir::IgnoreCase), QDir::Files);
+ if (epicManifests.exists()) {
+ QDirIterator it(epicManifests);
+ while (it.hasNext()) {
+ QString manifestFile = it.next();
+ QFile manifest(manifestFile);
+
+ if (!manifest.open(QIODevice::ReadOnly)) {
+ qWarning("Couldn't open Epic Games manifest file.");
+ continue;
+ }
+
+ QByteArray manifestData = manifest.readAll();
+
+ QJsonDocument manifestJson(QJsonDocument::fromJson(manifestData));
+
+ if (manifests.contains(manifestJson["AppName"].toString())) {
+ return manifestJson["InstallLocation"].toString();
+ }
+ }
+ }
+ return "";
+}
+
+QString GameGamebryo::parseSteamLocation(const QString& appid,
+ const QString& directoryName)
+{
+#ifdef HAS_NAK_FFI
+ // Use NaK to find Steam path
+ char* steamPathC = nak_find_steam_path();
+ QString steamLocation;
+ if (steamPathC) {
+ steamLocation = QString::fromUtf8(steamPathC);
+ nak_string_free(steamPathC);
+ }
+#elif defined(_WIN32)
+ QString path = "Software\\Valve\\Steam";
+ QString steamLocation =
+ findInRegistry(HKEY_CURRENT_USER, path.toStdWString().c_str(), L"SteamPath");
+#else
+ // Fallback: common Steam locations on Linux
+ QString steamLocation = QDir::homePath() + "/.steam/steam";
+ if (!QDir(steamLocation).exists()) {
+ steamLocation = QDir::homePath() + "/.local/share/Steam";
+ }
+#endif
+
+ if (!steamLocation.isEmpty()) {
+ QString steamLibraryLocation;
+ QString steamLibraries(steamLocation + "/" + "config" + "/" +
+ "libraryfolders.vdf");
+ if (QFile(steamLibraries).exists()) {
+ std::ifstream file(steamLibraries.toStdString());
+ auto root = tyti::vdf::read(file);
+ for (auto child : root.childs) {
+ tyti::vdf::object* library = child.second.get();
+ auto apps = library->childs["apps"];
+ if (apps->attribs.contains(appid.toStdString())) {
+ steamLibraryLocation = QString::fromStdString(library->attribs["path"]);
+ break;
+ }
+ }
+ }
+ if (!steamLibraryLocation.isEmpty()) {
+ if (!directoryName.isEmpty()) {
+ QString gameLocation = steamLibraryLocation + "/" + "steamapps" + "/" +
+ "common" + "/" + directoryName;
+ if (QDir(gameLocation).exists()) {
+ return gameLocation;
+ }
+ }
+
+ // Fallback: resolve install dir from appmanifest_<appid>.acf.
+ QString manifestPath = steamLibraryLocation + "/steamapps/appmanifest_" + appid +
+ ".acf";
+ if (QFile::exists(manifestPath)) {
+ std::ifstream manifestFile(manifestPath.toStdString());
+ auto manifest = tyti::vdf::read(manifestFile);
+ auto appStateIt = manifest.childs.find("AppState");
+ if (appStateIt != manifest.childs.end()) {
+ const auto& attrs = appStateIt->second->attribs;
+ auto installdirIt = attrs.find("installdir");
+ if (installdirIt != attrs.end()) {
+ QString installdir = QString::fromStdString(installdirIt->second);
+ QString gameLocation = steamLibraryLocation + "/steamapps/common/" + installdir;
+ if (QDir(gameLocation).exists()) {
+ return gameLocation;
+ }
+ }
+ }
+ }
+ }
+ }
+ return "";
+}
+
+void GameGamebryo::registerFeature(std::shared_ptr<MOBase::GameFeature> feature)
+{
+ // priority does not matter, this is a game plugin so will get lowest priority in MO2
+ m_Organizer->gameFeatures()->registerFeature(this, feature, 0, true);
+}
diff --git a/libs/game_bethesda/src/gamebryo/gamegamebryo.h b/libs/game_bethesda/src/gamebryo/gamegamebryo.h
new file mode 100644
index 0000000..fa0a13d
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/gamegamebryo.h
@@ -0,0 +1,169 @@
+#ifndef GAMEGAMEBRYO_H
+#define GAMEGAMEBRYO_H
+
+#include "iplugingame.h"
+
+class BSAInvalidation;
+class DataArchives;
+class LocalSavegames;
+class SaveGameInfo;
+class BSAInvalidation;
+class LocalSavegames;
+class ScriptExtender;
+class GamePlugins;
+class UnmanagedMods;
+
+#include <QObject>
+#include <QString>
+#ifdef _WIN32
+#include <ShlObj.h>
+#include <dbghelp.h>
+#endif
+#include <ipluginfilemapper.h>
+#include <iplugingame.h>
+#include <memory>
+
+#include "gamebryosavegame.h"
+#include "igamefeatures.h"
+
+class GameGamebryo : public MOBase::IPluginGame, public MOBase::IPluginFileMapper
+{
+ Q_OBJECT
+ Q_INTERFACES(MOBase::IPlugin MOBase::IPluginGame MOBase::IPluginFileMapper)
+
+ friend class GamebryoScriptExtender;
+ friend class GamebryoSaveGameInfo;
+ friend class GamebryoSaveGameInfoWidget;
+ friend class GamebryoSaveGame;
+
+ /**
+ * Some Bethesda games do not have a valid file version but a valid product
+ * version. If the file version starts with FALLBACK_GAME_VERSION, the product
+ * version will be tried.
+ */
+ static constexpr const char* FALLBACK_GAME_VERSION = "1.0.0";
+
+public:
+ GameGamebryo();
+
+ void detectGame() override;
+ bool init(MOBase::IOrganizer* moInfo) override;
+
+public: // IPluginGame interface
+ // getName
+ // initializeProfile
+ virtual std::vector<std::shared_ptr<const MOBase::ISaveGame>>
+ listSaves(QDir folder) const override;
+
+ virtual bool isInstalled() const override;
+ virtual QIcon gameIcon() const override;
+ virtual QDir gameDirectory() const override;
+ virtual QDir dataDirectory() const override;
+ // secondaryDataDirectories
+ virtual void setGamePath(const QString& path) override;
+ virtual QDir documentsDirectory() const override;
+ virtual QDir savesDirectory() const override;
+ // executables
+ // steamAPPId
+ // primaryPlugins
+ // enabledPlugins
+ // gameVariants
+ virtual void setGameVariant(const QString& variant) override;
+ virtual QString binaryName() const override;
+ // gameShortName
+ // primarySources
+ // validShortNames
+ // iniFiles
+ // DLCPlugins
+ // CCPlugins
+ virtual LoadOrderMechanism loadOrderMechanism() const override;
+ virtual SortMechanism sortMechanism() const override;
+ // nexusModOrganizerID
+ // nexusGameID
+ virtual bool looksValid(QDir const&) const override;
+ virtual QString gameVersion() const override;
+ virtual QString getLauncherName() const override;
+
+public: // IPluginFileMapper interface
+ virtual MappingType mappings() const;
+
+public: // Other (e.g. for game features)
+ QString myGamesPath() const;
+
+protected:
+ // Retrieve the saves extension for the game.
+ virtual QString savegameExtension() const = 0;
+ virtual QString savegameSEExtension() const = 0;
+
+ // Create a save game.
+ virtual std::shared_ptr<const GamebryoSaveGame>
+ makeSaveGame(QString filepath) const = 0;
+
+ QFileInfo findInGameFolder(const QString& relativePath) const;
+ QString selectedVariant() const;
+
+ static QString localAppFolder();
+ // Arguably this shouldn't really be here but every gamebryo program seems to
+ // use it
+ static QString getLootPath();
+
+ // This function is not terribly well named as it copies exactly where it's told
+ // to, irrespective of whether it's in the profile...
+ static void copyToProfile(const QString& sourcePath, const QDir& destinationDirectory,
+ const QString& sourceFileName);
+
+ static void copyToProfile(const QString& sourcePath, const QDir& destinationDirectory,
+ const QString& sourceFileName,
+ const QString& destinationFileName);
+
+ virtual QString identifyGamePath() const;
+
+ virtual bool prepareIni(const QString& exec);
+
+public:
+ // Ensure all game INI files exist with adequate content in the given
+ // base path. On Linux, seeds missing/stub INIs from the game's default
+ // INI templates and creates case-matching symlinks if needed.
+ void ensureIniFilesExist(const QString& basePath);
+
+protected:
+
+#ifdef _WIN32
+ static std::unique_ptr<BYTE[]> getRegValue(HKEY key, LPCWSTR path, LPCWSTR value,
+ DWORD flags, LPDWORD type);
+
+ static QString findInRegistry(HKEY baseKey, LPCWSTR path, LPCWSTR value);
+
+ static QString getKnownFolderPath(REFKNOWNFOLDERID folderId, bool useDefault);
+
+ static QString getSpecialPath(const QString& name);
+#endif
+
+ WORD getArch(QString const& program) const;
+
+ static QString determineMyGamesPath(const QString& gameName);
+
+ static QString parseEpicGamesLocation(const QStringList& manifests);
+
+ static QString parseSteamLocation(const QString& appid, const QString& directoryName);
+
+public: // Cross-platform INI file utilities (used by game features)
+ // Read a value from a Bethesda-style INI file using QSettings
+ static QString readIniValue(const QString& iniFile, const QString& section,
+ const QString& key, const QString& defaultValue = QString());
+
+protected:
+ void registerFeature(std::shared_ptr<MOBase::GameFeature> feature);
+
+protected:
+ // to access organizer for game features, avoid having to pass it to all saves since
+ // we already pass the game
+ friend class GamebryoSaveGame;
+
+ QString m_GamePath;
+ QString m_MyGamesPath;
+ QString m_GameVariant;
+ MOBase::IOrganizer* m_Organizer;
+};
+
+#endif // GAMEGAMEBRYO_H
diff --git a/libs/game_bethesda/src/gamebryo/vdf_parser.h b/libs/game_bethesda/src/gamebryo/vdf_parser.h
new file mode 100644
index 0000000..53994bf
--- /dev/null
+++ b/libs/game_bethesda/src/gamebryo/vdf_parser.h
@@ -0,0 +1,728 @@
+// MIT License
+//
+// Copyright(c) 2016 Matthias Moeller
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files(the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions :
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+#ifndef __TYTI_STEAM_VDF_PARSER_H__
+#define __TYTI_STEAM_VDF_PARSER_H__
+
+#include <algorithm>
+#include <fstream>
+#include <functional>
+#include <iterator>
+#include <map>
+#include <memory>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include <exception>
+#include <system_error>
+
+// for wstring support
+#include <locale>
+#include <string>
+
+// internal
+#include <stack>
+
+#include <QString>
+
+// VS < 2015 has only partial C++11 support
+#if defined(_MSC_VER) && _MSC_VER < 1900
+#ifndef CONSTEXPR
+#define CONSTEXPR
+#endif
+
+#ifndef NOEXCEPT
+#define NOEXCEPT
+#endif
+#else
+#ifndef CONSTEXPR
+#define CONSTEXPR constexpr
+#define TYTI_UNDEF_CONSTEXPR
+#endif
+
+#ifndef NOEXCEPT
+#define NOEXCEPT noexcept
+#define TYTI_UNDEF_NOEXCEPT
+#endif
+
+#endif
+
+namespace tyti
+{
+namespace vdf
+{
+ namespace detail
+ {
+ ///////////////////////////////////////////////////////////////////////////
+ // Helper functions selecting the right encoding (char/wchar_T)
+ ///////////////////////////////////////////////////////////////////////////
+
+ template <typename T>
+ struct literal_macro_help
+ {
+ static CONSTEXPR const char* result(const char* c, const wchar_t*) NOEXCEPT
+ {
+ return c;
+ }
+ static CONSTEXPR const char result(const char c, const wchar_t) NOEXCEPT
+ {
+ return c;
+ }
+ };
+
+ template <>
+ struct literal_macro_help<wchar_t>
+ {
+ static CONSTEXPR const wchar_t* result(const char*, const wchar_t* wc) NOEXCEPT
+ {
+ return wc;
+ }
+ static CONSTEXPR const wchar_t result(const char, const wchar_t wc) NOEXCEPT
+ {
+ return wc;
+ }
+ };
+#define TYTI_L(type, text) vdf::detail::literal_macro_help<type>::result(text, L##text)
+
+ inline std::string string_converter(const std::string& w) NOEXCEPT
+ {
+ return w;
+ }
+
+ inline std::string string_converter(const std::wstring& w)
+ {
+ return QString::fromStdWString(w).toStdString();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Writer helper functions
+ ///////////////////////////////////////////////////////////////////////////
+
+ template <typename charT>
+ class tabs
+ {
+ const size_t t;
+
+ public:
+ explicit CONSTEXPR tabs(size_t i) NOEXCEPT : t(i) {}
+ std::basic_string<charT> print() const
+ {
+ return std::basic_string<charT>(t, TYTI_L(charT, '\t'));
+ }
+ inline CONSTEXPR tabs operator+(size_t i) const NOEXCEPT { return tabs(t + i); }
+ };
+
+ template <typename oStreamT>
+ oStreamT& operator<<(oStreamT& s, const tabs<typename oStreamT::char_type> t)
+ {
+ s << t.print();
+ return s;
+ }
+ } // end namespace detail
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Interface
+ ///////////////////////////////////////////////////////////////////////////
+
+ /// custom objects and their corresponding write functions
+
+ /// basic object node. Every object has a name and can contains attributes saved as
+ /// key_value pairs or childrens
+ template <typename CharT>
+ struct basic_object
+ {
+ typedef CharT char_type;
+ std::basic_string<char_type> name;
+ std::unordered_map<std::basic_string<char_type>, std::basic_string<char_type>>
+ attribs;
+ std::unordered_map<std::basic_string<char_type>,
+ std::shared_ptr<basic_object<char_type>>>
+ childs;
+
+ void add_attribute(std::basic_string<char_type> key,
+ std::basic_string<char_type> value)
+ {
+ attribs.emplace(std::move(key), std::move(value));
+ }
+ void add_child(std::unique_ptr<basic_object<char_type>> child)
+ {
+ std::shared_ptr<basic_object<char_type>> obj{child.release()};
+ childs.emplace(obj->name, obj);
+ }
+ void set_name(std::basic_string<char_type> n) { name = std::move(n); }
+ };
+
+ template <typename CharT>
+ struct basic_multikey_object
+ {
+ typedef CharT char_type;
+ std::basic_string<char_type> name;
+ std::unordered_multimap<std::basic_string<char_type>, std::basic_string<char_type>>
+ attribs;
+ std::unordered_multimap<std::basic_string<char_type>,
+ std::shared_ptr<basic_multikey_object<char_type>>>
+ childs;
+
+ void add_attribute(std::basic_string<char_type> key,
+ std::basic_string<char_type> value)
+ {
+ attribs.emplace(std::move(key), std::move(value));
+ }
+ void add_child(std::unique_ptr<basic_multikey_object<char_type>> child)
+ {
+ std::shared_ptr<basic_multikey_object<char_type>> obj{child.release()};
+ childs.emplace(obj->name, obj);
+ }
+ void set_name(std::basic_string<char_type> n) { name = std::move(n); }
+ };
+
+ typedef basic_object<char> object;
+ typedef basic_object<wchar_t> wobject;
+ typedef basic_multikey_object<char> multikey_object;
+ typedef basic_multikey_object<wchar_t> wmultikey_object;
+
+ struct Options
+ {
+ bool strip_escape_symbols;
+ bool ignore_all_platform_conditionals;
+ bool ignore_includes;
+
+ Options()
+ : strip_escape_symbols(true), ignore_all_platform_conditionals(false),
+ ignore_includes(false)
+ {}
+ };
+
+ // forward decls
+ // forward decl
+ template <typename OutputT, typename iStreamT>
+ OutputT read(iStreamT& inStream, const Options& opt = Options{});
+
+ /** \brief writes given object tree in vdf format to given stream.
+ Output is prettyfied, using tabs
+ */
+ template <typename oStreamT, typename T>
+ void write(oStreamT& s, const T& r,
+ const detail::tabs<typename oStreamT::char_type> tab =
+ detail::tabs<typename oStreamT::char_type>(0))
+ {
+ typedef typename oStreamT::char_type charT;
+ using namespace detail;
+ s << tab << TYTI_L(charT, '"') << r.name << TYTI_L(charT, "\"\n") << tab
+ << TYTI_L(charT, "{\n");
+ for (const auto& i : r.attribs)
+ s << tab + 1 << TYTI_L(charT, '"') << i.first << TYTI_L(charT, "\"\t\t\"")
+ << i.second << TYTI_L(charT, "\"\n");
+ for (const auto& i : r.childs)
+ if (i.second)
+ write(s, *i.second, tab + 1);
+ s << tab << TYTI_L(charT, "}\n");
+ }
+
+ namespace detail
+ {
+ template <typename iStreamT>
+ std::basic_string<typename iStreamT::char_type> read_file(iStreamT& inStream)
+ {
+ // cache the file
+ typedef typename iStreamT::char_type charT;
+ std::basic_string<charT> str;
+ inStream.seekg(0, std::ios::end);
+ str.resize(static_cast<size_t>(inStream.tellg()));
+ if (str.empty())
+ return str;
+
+ inStream.seekg(0, std::ios::beg);
+ inStream.read(&str[0], str.size());
+ return str;
+ }
+
+ /** \brief Read VDF formatted sequences defined by the range [first, last).
+ If the file is mailformatted, parser will try to read it until it can.
+ @param first begin iterator
+ @param end end iterator
+ @param exclude_files list of files which cant be included anymore.
+ prevents circular includes
+
+ can thow:
+ - "std::runtime_error" if a parsing error occured
+ - "std::bad_alloc" if not enough memory coup be allocated
+ */
+ template <typename OutputT, typename IterT>
+ std::vector<std::unique_ptr<OutputT>>
+ read_internal(IterT first, const IterT last,
+ std::unordered_set<std::basic_string<
+ typename std::iterator_traits<IterT>::value_type>>& exclude_files,
+ const Options& opt)
+ {
+ static_assert(std::is_default_constructible<OutputT>::value,
+ "Output Type must be default constructible (provide constructor "
+ "without arguments)");
+ static_assert(std::is_move_constructible<OutputT>::value,
+ "Output Type must be move constructible");
+
+ typedef typename std::iterator_traits<IterT>::value_type charT;
+
+ const std::basic_string<charT> comment_end_str = TYTI_L(charT, "*/");
+ const std::basic_string<charT> whitespaces = TYTI_L(charT, " \n\v\f\r\t");
+
+#ifdef WIN32
+ std::function<bool(const std::basic_string<charT>&)> is_platform_str =
+ [](const std::basic_string<charT>& in) {
+ return in == TYTI_L(charT, "$WIN32") || in == TYTI_L(charT, "$WINDOWS");
+ };
+#elif __APPLE__
+ // WIN32 stands for pc in general
+ std::function<bool(const std::basic_string<charT>&)> is_platform_str =
+ [](const std::basic_string<charT>& in) {
+ return in == TYTI_L(charT, "$WIN32") || in == TYTI_L(charT, "$POSIX") ||
+ in == TYTI_L(charT, "$OSX");
+ };
+
+#elif __linux__
+ // WIN32 stands for pc in general
+ std::function<bool(const std::basic_string<charT>&)> is_platform_str =
+ [](const std::basic_string<charT>& in) {
+ return in == TYTI_L(charT, "$WIN32") || in == TYTI_L(charT, "$POSIX") ||
+ in == TYTI_L(charT, "$LINUX");
+ };
+#else
+ std::function<bool(const std::basic_string<charT>&)> is_platform_str =
+ [](const std::basic_string<charT>& in) {
+ return false;
+ };
+#endif
+
+ if (opt.ignore_all_platform_conditionals)
+ is_platform_str = [](const std::basic_string<charT>&) {
+ return false;
+ };
+
+ // function for skipping a comment block
+ // iter: iterator poition to the position after a '/'
+ auto skip_comments = [&comment_end_str](IterT iter, const IterT& last) -> IterT {
+ ++iter;
+ if (iter != last) {
+ if (*iter == TYTI_L(charT, '/')) {
+ // line comment, skip whole line
+ iter = std::find(iter + 1, last, TYTI_L(charT, '\n'));
+ }
+
+ if (*iter == '*') {
+ // block comment, skip until next occurance of "*\"
+ iter = std::search(iter + 1, last, std::begin(comment_end_str),
+ std::end(comment_end_str));
+ iter += 2;
+ }
+ }
+ return iter;
+ };
+
+ auto end_quote = [](IterT iter, const IterT& last) -> IterT {
+ const auto begin = iter;
+ auto last_esc = iter;
+ do {
+ ++iter;
+ iter = std::find(iter, last, TYTI_L(charT, '\"'));
+ if (iter == last)
+ break;
+
+ last_esc = std::prev(iter);
+ while (last_esc != begin && *last_esc == '\\')
+ --last_esc;
+ } while (!(std::distance(last_esc, iter) % 2));
+ if (iter == last)
+ throw std::runtime_error{"quote was opened but not closed."};
+ return iter;
+ };
+
+ auto end_word = [&whitespaces](IterT iter, const IterT& last) -> IterT {
+ const auto begin = iter;
+ auto last_esc = iter;
+ do {
+ ++iter;
+ iter = std::find_first_of(iter, last, std::begin(whitespaces),
+ std::end(whitespaces));
+ if (iter == last)
+ break;
+
+ last_esc = std::prev(iter);
+ while (last_esc != begin && *last_esc == '\\')
+ --last_esc;
+ } while (!(std::distance(last_esc, iter) % 2));
+ // if (iter == last)
+ // throw std::runtime_error{ "word wasnt properly ended" };
+ return iter;
+ };
+
+ auto skip_whitespaces = [&whitespaces](IterT iter, const IterT& last) -> IterT {
+ iter = std::find_if_not(iter, last, [&whitespaces](charT c) {
+ // return true if whitespace
+ return std::any_of(std::begin(whitespaces), std::end(whitespaces),
+ [c](charT pc) {
+ return pc == c;
+ });
+ });
+ return iter;
+ };
+
+ std::function<void(std::basic_string<charT>&)> strip_escape_symbols =
+ [](std::basic_string<charT>& s) {
+ auto quote_searcher = [&s](size_t pos) {
+ return s.find(TYTI_L(charT, "\\\""), pos);
+ };
+ auto p = quote_searcher(0);
+ while (p != s.npos) {
+ s.replace(p, 2, TYTI_L(charT, "\""));
+ p = quote_searcher(p);
+ }
+ auto searcher = [&s](size_t pos) {
+ return s.find(TYTI_L(charT, "\\\\"), pos);
+ };
+ p = searcher(0);
+ while (p != s.npos) {
+ s.replace(p, 2, TYTI_L(charT, "\\"));
+ p = searcher(p);
+ }
+ };
+
+ if (!opt.strip_escape_symbols)
+ strip_escape_symbols = [](std::basic_string<charT>&) {};
+
+ auto conditional_fullfilled = [&skip_whitespaces,
+ &is_platform_str](IterT& iter, const IterT& last) {
+ iter = skip_whitespaces(iter, last);
+ if (*iter == '[') {
+ ++iter;
+ const auto end = std::find(iter, last, ']');
+ const bool negate = *iter == '!';
+ if (negate)
+ ++iter;
+ auto conditional = std::basic_string<charT>(iter, end);
+
+ const bool is_platform = is_platform_str(conditional);
+ iter = end + 1;
+
+ return static_cast<bool>(is_platform ^ negate);
+ }
+ return true;
+ };
+
+ // read header
+ // first, quoted name
+ std::unique_ptr<OutputT> curObj = nullptr;
+ std::vector<std::unique_ptr<OutputT>> roots;
+ std::stack<std::unique_ptr<OutputT>> lvls;
+ auto curIter = first;
+
+ while (curIter != last && *curIter != '\0') {
+ // find first starting attrib/child, or ending
+ curIter = skip_whitespaces(curIter, last);
+ if (curIter == last || *curIter == '\0')
+ break;
+ if (*curIter == TYTI_L(charT, '/')) {
+ curIter = skip_comments(curIter, last);
+ } else if (*curIter != TYTI_L(charT, '}')) {
+
+ // get key
+ const auto keyEnd = (*curIter == TYTI_L(charT, '\"'))
+ ? end_quote(curIter, last)
+ : end_word(curIter, last);
+ if (*curIter == TYTI_L(charT, '\"'))
+ ++curIter;
+ std::basic_string<charT> key(curIter, keyEnd);
+ strip_escape_symbols(key);
+ curIter = keyEnd + ((*keyEnd == TYTI_L(charT, '\"')) ? 1 : 0);
+
+ curIter = skip_whitespaces(curIter, last);
+
+ auto conditional = conditional_fullfilled(curIter, last);
+ if (!conditional)
+ continue;
+
+ while (*curIter == TYTI_L(charT, '/')) {
+
+ curIter = skip_comments(curIter, last);
+ if (curIter == last || *curIter == '}')
+ throw std::runtime_error{"key declared, but no value"};
+ curIter = skip_whitespaces(curIter, last);
+ if (curIter == last || *curIter == '}')
+ throw std::runtime_error{"key declared, but no value"};
+ }
+ // get value
+ if (*curIter != '{') {
+ const auto valueEnd = (*curIter == TYTI_L(charT, '\"'))
+ ? end_quote(curIter, last)
+ : end_word(curIter, last);
+ if (*curIter == TYTI_L(charT, '\"'))
+ ++curIter;
+
+ auto value = std::basic_string<charT>(curIter, valueEnd);
+ strip_escape_symbols(value);
+ curIter = valueEnd + ((*valueEnd == TYTI_L(charT, '\"')) ? 1 : 0);
+
+ auto conditional = conditional_fullfilled(curIter, last);
+ if (!conditional)
+ continue;
+
+ // process value
+ if (key != TYTI_L(charT, "#include") && key != TYTI_L(charT, "#base")) {
+ if (curObj) {
+ curObj->add_attribute(std::move(key), std::move(value));
+ } else {
+ throw std::runtime_error{"unexpected key without object"};
+ }
+ } else {
+ if (!opt.ignore_includes &&
+ exclude_files.find(value) == exclude_files.end()) {
+ exclude_files.insert(value);
+ std::basic_ifstream<charT> i(detail::string_converter(value));
+ auto str = read_file(i);
+ auto file_objs =
+ read_internal<OutputT>(str.begin(), str.end(), exclude_files, opt);
+ for (auto& n : file_objs) {
+ if (curObj)
+ curObj->add_child(std::move(n));
+ else
+ roots.push_back(std::move(n));
+ }
+ exclude_files.erase(value);
+ }
+ }
+ } else if (*curIter == '{') {
+ if (curObj)
+ lvls.push(std::move(curObj));
+ curObj = std::make_unique<OutputT>();
+ curObj->set_name(std::move(key));
+ ++curIter;
+ }
+ }
+ // end of new object
+ else if (curObj && *curIter == TYTI_L(charT, '}')) {
+ if (!lvls.empty()) {
+ // get object before
+ std::unique_ptr<OutputT> prev{std::move(lvls.top())};
+ lvls.pop();
+
+ // add finished obj to obj before and release it from processing
+ prev->add_child(std::move(curObj));
+ curObj = std::move(prev);
+ } else {
+ roots.push_back(std::move(curObj));
+ curObj.reset();
+ }
+ ++curIter;
+ } else {
+ throw std::runtime_error{"unexpected '}'"};
+ }
+ }
+ if (curObj != nullptr || !lvls.empty()) {
+ throw std::runtime_error{"object is not closed with '}'"};
+ }
+
+ return roots;
+ }
+
+ } // namespace detail
+
+ /** \brief Read VDF formatted sequences defined by the range [first, last).
+ If the file is mailformatted, parser will try to read it until it can.
+ @param first begin iterator
+ @param end end iterator
+
+ can thow:
+ - "std::runtime_error" if a parsing error occured
+ - "std::bad_alloc" if not enough memory coup be allocated
+ */
+ template <typename OutputT, typename IterT>
+ OutputT read(IterT first, const IterT last, const Options& opt = Options{})
+ {
+ auto exclude_files = std::unordered_set<
+ std::basic_string<typename std::iterator_traits<IterT>::value_type>>{};
+ auto roots = detail::read_internal<OutputT>(first, last, exclude_files, opt);
+
+ OutputT result;
+ if (roots.size() > 1) {
+ for (auto& i : roots)
+ result.add_child(std::move(i));
+ } else if (roots.size() == 1)
+ result = std::move(*roots[0]);
+
+ return result;
+ }
+
+ /** \brief Read VDF formatted sequences defined by the range [first, last).
+ If the file is mailformatted, parser will try to read it until it can.
+ @param first begin iterator
+ @param end end iterator
+ @param ec output bool. 0 if ok, otherwise, holds an system error code
+
+ Possible error codes:
+ std::errc::protocol_error: file is mailformatted
+ std::errc::not_enough_memory: not enough space
+ std::errc::invalid_argument: iterators throws e.g. out of range
+ */
+ template <typename OutputT, typename IterT>
+ OutputT read(IterT first, IterT last, std::error_code& ec,
+ const Options& opt = Options{}) NOEXCEPT
+
+ {
+ ec.clear();
+ OutputT r{};
+ try {
+ r = read<OutputT>(first, last, opt);
+ } catch (std::runtime_error&) {
+ ec = std::make_error_code(std::errc::protocol_error);
+ } catch (std::bad_alloc&) {
+ ec = std::make_error_code(std::errc::not_enough_memory);
+ } catch (...) {
+ ec = std::make_error_code(std::errc::invalid_argument);
+ }
+ return r;
+ }
+
+ /** \brief Read VDF formatted sequences defined by the range [first, last).
+ If the file is mailformatted, parser will try to read it until it can.
+ @param first begin iterator
+ @param end end iterator
+ @param ok output bool. true, if parser successed, false, if parser failed
+ */
+ template <typename OutputT, typename IterT>
+ OutputT read(IterT first, const IterT last, bool* ok,
+ const Options& opt = Options{}) NOEXCEPT
+ {
+ std::error_code ec;
+ auto r = read<OutputT>(first, last, ec, opt);
+ if (ok)
+ *ok = !ec;
+ return r;
+ }
+
+ template <typename IterT>
+ inline auto read(IterT first, const IterT last, bool* ok,
+ const Options& opt = Options{}) NOEXCEPT
+ -> basic_object<typename std::iterator_traits<IterT>::value_type>
+ {
+ return read<basic_object<typename std::iterator_traits<IterT>::value_type>>(
+ first, last, ok, opt);
+ }
+
+ template <typename IterT>
+ inline auto read(IterT first, IterT last, std::error_code& ec,
+ const Options& opt = Options{}) NOEXCEPT
+ -> basic_object<typename std::iterator_traits<IterT>::value_type>
+ {
+ return read<basic_object<typename std::iterator_traits<IterT>::value_type>>(
+ first, last, ec, opt);
+ }
+
+ template <typename IterT>
+ inline auto read(IterT first, const IterT last, const Options& opt = Options{})
+ -> basic_object<typename std::iterator_traits<IterT>::value_type>
+ {
+ return read<basic_object<typename std::iterator_traits<IterT>::value_type>>(
+ first, last, opt);
+ }
+
+ /** \brief Loads a stream (e.g. filestream) into the memory and parses the vdf
+ formatted data. throws "std::bad_alloc" if file buffer could not be allocated
+ */
+ template <typename OutputT, typename iStreamT>
+ OutputT read(iStreamT& inStream, std::error_code& ec, const Options& opt = Options{})
+ {
+ // cache the file
+ typedef typename iStreamT::char_type charT;
+ std::basic_string<charT> str = detail::read_file(inStream);
+
+ // parse it
+ return read<OutputT>(str.begin(), str.end(), ec, opt);
+ }
+
+ template <typename iStreamT>
+ inline basic_object<typename iStreamT::char_type>
+ read(iStreamT& inStream, std::error_code& ec, const Options& opt = Options{})
+ {
+ return read<basic_object<typename iStreamT::char_type>>(inStream, ec, opt);
+ }
+
+ /** \brief Loads a stream (e.g. filestream) into the memory and parses the vdf
+ formatted data. throws "std::bad_alloc" if file buffer could not be allocated ok ==
+ false, if a parsing error occured
+ */
+ template <typename OutputT, typename iStreamT>
+ OutputT read(iStreamT& inStream, bool* ok, const Options& opt = Options{})
+ {
+ std::error_code ec;
+ const auto r = read<OutputT>(inStream, ec, opt);
+ if (ok)
+ *ok = !ec;
+ return r;
+ }
+
+ template <typename iStreamT>
+ inline basic_object<typename iStreamT::char_type> read(iStreamT& inStream, bool* ok,
+ const Options& opt = Options{})
+ {
+ return read<basic_object<typename iStreamT::char_type>>(inStream, ok, opt);
+ }
+
+ /** \brief Loads a stream (e.g. filestream) into the memory and parses the vdf
+ formatted data. throws "std::bad_alloc" if file buffer could not be allocated
+ throws "std::runtime_error" if a parsing error occured
+ */
+ template <typename OutputT, typename iStreamT>
+ OutputT read(iStreamT& inStream, const Options& opt)
+ {
+
+ // cache the file
+ typedef typename iStreamT::char_type charT;
+ std::basic_string<charT> str = detail::read_file(inStream);
+ // parse it
+ return read<OutputT>(str.begin(), str.end(), opt);
+ }
+
+ template <typename iStreamT>
+ inline basic_object<typename iStreamT::char_type> read(iStreamT& inStream,
+ const Options& opt = Options{})
+ {
+ return read<basic_object<typename iStreamT::char_type>>(inStream, opt);
+ }
+
+} // namespace vdf
+} // namespace tyti
+#ifndef TYTI_NO_L_UNDEF
+#undef TYTI_L
+#endif
+
+#ifdef TYTI_UNDEF_CONSTEXPR
+#undef CONSTEXPR
+#undef TYTI_NO_L_UNDEF
+#endif
+
+#ifdef TYTI_UNDEF_NOTHROW
+#undef NOTHROW
+#undef TYTI_UNDEF_NOTHROW
+#endif
+
+#endif //__TYTI_STEAM_VDF_PARSER_H__
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 <memory>
+
+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 <utility.h>
+
+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 <QDir>
+#include <QString>
+#include <QStringList>
+#include <gamebryodataarchives.h>
+#include <iprofile.h>
+
+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 <ipluginlist.h>
+#include <report.h>
+#include <safewritefile.h>
+#include <scopeguard.h>
+
+#include <QDir>
+#include <QStringEncoder>
+#include <QStringList>
+
+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 <gamebryogameplugins.h>
+#include <imoinfo.h>
+#include <iplugingame.h>
+#include <map>
+
+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<QString, QByteArray> 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 <gamebryolocalsavegames.h>
+
+#include <QString>
+
+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 <gamebryomoddatachecker.h>
+
+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 <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+// 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 <Windows.h>
+#else
+#include <QDateTime>
+
+using SYSTEMTIME = _SYSTEMTIME;
+
+static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st)
+{
+ const uint64_t ticks = (static_cast<uint64_t>(ft->dwHighDateTime) << 32) |
+ static_cast<uint64_t>(ft->dwLowDateTime);
+ const int64_t unixSecs = static_cast<int64_t>(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<uint16_t>(d.year());
+ st->wMonth = static_cast<uint16_t>(d.month());
+ st->wDayOfWeek = static_cast<uint16_t>(d.dayOfWeek() % 7);
+ st->wDay = static_cast<uint16_t>(d.day());
+ st->wHour = static_cast<uint16_t>(t.hour());
+ st->wMinute = static_cast<uint16_t>(t.minute());
+ st->wSecond = static_cast<uint16_t>(t.second());
+ st->wMilliseconds = static_cast<uint16_t>(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<uint32_t>(); // header size
+ file.skip<uint32_t>(); // header version
+ file.read(saveNumber);
+
+ file.read(playerName);
+
+ uint32_t temp;
+ file.read(temp);
+ playerLevel = static_cast<unsigned short>(temp);
+
+ file.read(playerLocation);
+
+ QString timeOfDay;
+ file.read(timeOfDay);
+
+ QString race;
+ file.read(race); // race name (i.e. BretonRace)
+
+ file.skip<unsigned short>(); // Player gender (0 = male)
+ file.skip<float>(2); // experience gathered, experience required
+
+ file.read(creationTime);
+}
+
+std::unique_ptr<GamebryoSaveGame::DataFields> EnderalSaveGame::fetchDataFields() const
+{
+ FileWrapper file(getFilepath(), "TESV_SAVEGAME");
+ std::unique_ptr<DataFields> fields = std::make_unique<DataFields>();
+
+ {
+ QString dummyName, dummyLocation;
+ unsigned short dummyLevel;
+ uint32_t dummySaveNumber;
+ FILETIME dummyTime;
+
+ fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation,
+ dummyTime);
+ }
+
+ fields->Screenshot = file.readImage();
+
+ file.skip<unsigned char>(); // form version
+ file.skip<uint32_t>(); // 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<DataFields> 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 <QString>
+#include <QStringList>
+
+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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameEnderal</name>
+ <message>
+ <location filename="gameenderal.cpp" line="93"/>
+ <source>Enderal Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gameenderal.cpp" line="103"/>
+ <source>Adds support for the game Enderal</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <gamebryogameplugins.h>
+#include <gamebryosavegameinfo.h>
+#include <gamebryounmanagedmods.h>
+
+#include <QCoreApplication>
+#include <QDebug>
+#include <QFileInfo>
+
+#include <QIcon>
+#include <QtDebug>
+
+#ifdef _WIN32
+#include <Windows.h>
+#include <winver.h>
+#endif
+
+#include <exception>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+using namespace MOBase;
+
+GameEnderal::GameEnderal() {}
+
+bool GameEnderal::init(IOrganizer* moInfo)
+{
+ if (!GameGamebryo::init(moInfo)) {
+ return false;
+ }
+
+ auto dataArchives = std::make_shared<EnderalDataArchives>(this);
+ registerFeature(std::make_shared<EnderalScriptExtender>(this));
+ registerFeature(dataArchives);
+ registerFeature(std::make_shared<EnderalBSAInvalidation>(dataArchives.get(), this));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(std::make_shared<EnderalLocalSavegames>(this, "enderal.ini"));
+ registerFeature(std::make_shared<EnderalModDataChecker>(this));
+ registerFeature(std::make_shared<EnderalModDataContent>(moInfo->gameFeatures()));
+ registerFeature(std::make_shared<EnderalGamePlugins>(moInfo));
+ registerFeature(std::make_shared<GamebryoUnmangedMods>(this));
+
+ return true;
+}
+
+QString GameEnderal::gameName() const
+{
+ return "Enderal";
+}
+
+QList<ExecutableInfo> GameEnderal::executables() const
+{
+ return QList<ExecutableInfo>()
+ //<< ExecutableInfo("SKSE",
+ // findInGameFolder(feature<ScriptExtender>()->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<ExecutableForcedLoadSetting> GameEnderal::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+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<PluginSetting> GameEnderal::settings() const
+{
+ QList<PluginSetting> 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<const GamebryoSaveGame>
+GameEnderal::makeSaveGame(QString filepath) const
+{
+ return std::make_shared<const EnderalSaveGame>(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 <QObject>
+#include <QtGlobal>
+
+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<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ 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<MOBase::PluginSetting> settings() const override;
+
+protected:
+ virtual QString savegameExtension() const override;
+ virtual QString savegameSEExtension() const override;
+ virtual std::shared_ptr<const GamebryoSaveGame>
+ 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 <utility.h>
+
+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 <QDir>
+#include <QStringList>
+
+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 <ipluginlist.h>
+#include <log.h>
+#include <report.h>
+#include <safewritefile.h>
+
+#include <QStringEncoder>
+
+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<QString> ManagedMods =
+ QSet<QString>(PrimaryPlugins.begin(), PrimaryPlugins.end());
+ QSet<QString> DLCSet = QSet<QString>(DLCPlugins.begin(), DLCPlugins.end());
+ ManagedMods.subtract(DLCSet);
+ PrimaryPlugins.append(QList<QString>(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 <creationgameplugins.h>
+#include <imoinfo.h>
+#include <iplugingame.h>
+#include <map>
+
+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 <gamebryolocalsavegames.h>
+
+#include <QString>
+
+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 <gamebryomoddatachecker.h>
+
+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 <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+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 <Windows.h>
+#else
+#include <QDateTime>
+
+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<uint64_t>(ft->dwHighDateTime) << 32) |
+ static_cast<uint64_t>(ft->dwLowDateTime);
+ const int64_t unixSecs = static_cast<int64_t>(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<uint16_t>(d.year());
+ st->wMonth = static_cast<uint16_t>(d.month());
+ st->wDayOfWeek = static_cast<uint16_t>(d.dayOfWeek() % 7);
+ st->wDay = static_cast<uint16_t>(d.day());
+ st->wHour = static_cast<uint16_t>(t.hour());
+ st->wMinute = static_cast<uint16_t>(t.minute());
+ st->wSecond = static_cast<uint16_t>(t.second());
+ st->wMilliseconds = static_cast<uint16_t>(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<unsigned short>(temp);
+ file.read(playerLocation);
+
+ QString timeOfDay;
+ file.read(timeOfDay);
+
+ QString race;
+ file.read(race); // race name (i.e. BretonRace)
+
+ file.skip<unsigned short>(); // Player gender (0 = male)
+ file.skip<float>(2); // experience gathered, experience required
+
+ file.read(creationTime); // filetime
+}
+
+std::unique_ptr<GamebryoSaveGame::DataFields> 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<DataFields> fields = std::make_unique<DataFields>();
+
+ 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<DataFields> 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 <QString>
+#include <QStringList>
+
+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 <gamegamebryo.h>
+
+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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameEnderalSE</name>
+ <message>
+ <location filename="gameenderalse.cpp" line="185"/>
+ <source>Enderal Special Edition Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gameenderalse.cpp" line="195"/>
+ <source>Adds support for the game Enderal Special Edition.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>QObject</name>
+ <message>
+ <location filename="enderalsegameplugins.cpp" line="75"/>
+ <source>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.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <executableinfo.h>
+#include <gamebryosavegameinfo.h>
+#include <ipluginlist.h>
+#include <pluginsetting.h>
+#include <utility.h>
+
+#include <QCoreApplication>
+#include <QDir>
+#include <QFileInfo>
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <QStringList>
+
+#include "scopeguard.h"
+#include <memory>
+
+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<QString, QString> 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<EnderalSEDataArchives>(this);
+ registerFeature(std::make_shared<EnderalSEScriptExtender>(this));
+ registerFeature(dataArchives);
+ registerFeature(std::make_shared<EnderalSELocalSavegames>(this, "enderal.ini"));
+ registerFeature(std::make_shared<EnderalSEModDataChecker>(this));
+ registerFeature(std::make_shared<EnderalSEModDataContent>(moInfo->gameFeatures()));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(std::make_shared<EnderalSEGamePlugins>(moInfo));
+ registerFeature(std::make_shared<EnderalSEUnmangedMods>(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<ExecutableInfo> GameEnderalSE::executables() const
+{
+ return QList<ExecutableInfo>()
+ << ExecutableInfo("Enderal Special Edition (SKSE)",
+ findInGameFolder(m_Organizer->gameFeatures()
+ ->gameFeature<MOBase::ScriptExtender>()
+ ->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<ExecutableForcedLoadSetting> GameEnderalSE::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+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<PluginSetting> GameEnderalSE::settings() const
+{
+ return QList<PluginSetting>();
+}
+
+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<const GamebryoSaveGame>
+GameEnderalSE::makeSaveGame(QString filePath) const
+{
+ return std::make_shared<const EnderalSESaveGame>(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 <QObject>
+#include <QtGlobal>
+
+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<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ 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<MOBase::PluginSetting> settings() const override;
+ virtual MappingType mappings() const override;
+
+protected:
+ std::shared_ptr<const GamebryoSaveGame> 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 <memory>
+
+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 <utility.h>
+
+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 <QDir>
+
+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 <gamebryomoddatachecker.h>
+
+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 <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+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<uint32_t>(); // Save header size
+
+ file.setHasFieldMarkers(true);
+ file.setPluginString(GamebryoSaveGame::StringType::TYPE_BZSTRING);
+
+ file.skip<uint32_t>(); // File version ?
+ file.skip<unsigned char>(); // 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<GamebryoSaveGame::DataFields> Fallout3SaveGame::fetchDataFields() const
+{
+ FileWrapper file(getFilepath(), "FO3SAVEGAME");
+
+ std::unique_ptr<DataFields> fields = std::make_unique<DataFields>();
+
+ 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<char>(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<DataFields> 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 <QString>
+#include <QStringList>
+
+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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameFallout3</name>
+ <message>
+ <location filename="gamefallout3.cpp" line="138"/>
+ <source>Fallout 3 Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamefallout3.cpp" line="148"/>
+ <source>Adds support for the game Fallout 3.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <gamebryogameplugins.h>
+#include <gamebryolocalsavegames.h>
+#include <gamebryosavegameinfo.h>
+#include <gamebryounmanagedmods.h>
+
+#include <QCoreApplication>
+#include <QDir>
+#include <QFileInfo>
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <QStringList>
+
+#include <memory>
+
+using namespace MOBase;
+
+GameFallout3::GameFallout3() {}
+
+bool GameFallout3::init(IOrganizer* moInfo)
+{
+ if (!GameGamebryo::init(moInfo)) {
+ return false;
+ }
+
+ auto dataArchives = std::make_shared<Fallout3DataArchives>(this);
+ registerFeature(std::make_shared<Fallout3ScriptExtender>(this));
+ registerFeature(dataArchives);
+ registerFeature(std::make_shared<Fallout3BSAInvalidation>(dataArchives.get(), this));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "fallout.ini"));
+ registerFeature(std::make_shared<Fallout3ModDataChecker>(this));
+ registerFeature(
+ std::make_shared<Fallout3ModDataContent>(m_Organizer->gameFeatures()));
+ registerFeature(std::make_shared<GamebryoGamePlugins>(moInfo));
+ registerFeature(std::make_shared<GamebryoUnmangedMods>(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<ExecutableInfo> GameFallout3::executables() const
+{
+ return QList<ExecutableInfo>()
+ << ExecutableInfo("FOSE",
+ findInGameFolder(m_Organizer->gameFeatures()
+ ->gameFeature<MOBase::ScriptExtender>()
+ ->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<ExecutableForcedLoadSetting> GameFallout3::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+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<PluginSetting> GameFallout3::settings() const
+{
+ return QList<PluginSetting>();
+}
+
+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<const GamebryoSaveGame>
+GameFallout3::makeSaveGame(QString filePath) const
+{
+ return std::make_shared<const Fallout3SaveGame>(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<QString, QString> 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 <QObject>
+#include <QtGlobal>
+
+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<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ 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<MOBase::PluginSetting> settings() const override;
+
+protected:
+ virtual QString identifyGamePath() const override;
+ virtual QString savegameExtension() const override;
+ virtual QString savegameSEExtension() const override;
+ std::shared_ptr<const GamebryoSaveGame> 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 @@
+<RCC>
+ <qresource prefix="/Fallout4">
+ <file alias="splash">splash.png</file>
+ </qresource>
+</RCC>
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 <imoinfo.h>
+#include <utility.h>
+
+#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 <bsainvalidation.h>
+#include <dataarchives.h>
+
+#include <memory>
+
+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 <utility.h>
+
+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 <QDir>
+#include <QStringList>
+
+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 <gamebryomoddatachecker.h>
+
+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 <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+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<Content> getAllContents() const override
+ {
+ auto contents = GamebryoModDataContent::getAllContents();
+ contents.push_back(
+ Content(CONTENT_MATERIAL, "Materials", ":/MO/gui/content/material"));
+ return contents;
+ }
+
+ std::vector<int>
+ getContentsFor(std::shared_ptr<const MOBase::IFileTree> 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 <Windows.h>
+#else
+#include <QDateTime>
+
+using SYSTEMTIME = _SYSTEMTIME;
+
+static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st)
+{
+ const uint64_t ticks = (static_cast<uint64_t>(ft->dwHighDateTime) << 32) |
+ static_cast<uint64_t>(ft->dwLowDateTime);
+ const int64_t unixSecs = static_cast<int64_t>(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<uint16_t>(d.year());
+ st->wMonth = static_cast<uint16_t>(d.month());
+ st->wDayOfWeek = static_cast<uint16_t>(d.dayOfWeek() % 7);
+ st->wDay = static_cast<uint16_t>(d.day());
+ st->wHour = static_cast<uint16_t>(t.hour());
+ st->wMinute = static_cast<uint16_t>(t.minute());
+ st->wSecond = static_cast<uint16_t>(t.second());
+ st->wMilliseconds = static_cast<uint16_t>(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<uint32_t>(); // header size
+ file.skip<uint32_t>(); // header version
+ file.read(saveNumber);
+
+ file.read(playerName);
+
+ uint32_t temp;
+ file.read(temp);
+ playerLevel = static_cast<unsigned short>(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<unsigned short>(); // Player gender (0 = male)
+ file.skip<float>(2); // experience gathered, experience required
+
+ file.read(creationTime);
+}
+
+std::unique_ptr<GamebryoSaveGame::DataFields> 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<DataFields> fields = std::make_unique<DataFields>();
+
+ fields->Screenshot = file.readImage(384, true);
+
+ uint8_t saveGameVersion = file.readChar();
+ file.read(ignore); // game version
+ file.skip<uint32_t>(); // 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<DataFields> 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 <QString>
+#include <QStringList>
+
+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 <gamegamebryo.h>
+
+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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameFallout4</name>
+ <message>
+ <location filename="gamefallout4.cpp" line="94"/>
+ <source>Fallout 4 Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamefallout4.cpp" line="104"/>
+ <source>Adds support for the game Fallout 4.
+Splash by %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamefallout4.cpp" line="321"/>
+ <source>sTestFile entries are present</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamefallout4.cpp" line="329"/>
+ <source>&lt;p&gt;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.&lt;/p&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <creationgameplugins.h>
+#include <executableinfo.h>
+#include <gamebryolocalsavegames.h>
+#include <gamebryosavegameinfo.h>
+#include <pluginsetting.h>
+
+#include <QCoreApplication>
+#include <QDir>
+#include <QFileInfo>
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <QStringList>
+
+#include <memory>
+
+#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<Fallout4DataArchives>(this);
+
+ registerFeature(std::make_shared<Fallout4ScriptExtender>(this));
+ registerFeature(dataArchives);
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "fallout4custom.ini"));
+ registerFeature(std::make_shared<Fallout4ModDataChecker>(this));
+ registerFeature(
+ std::make_shared<Fallout4ModDataContent>(m_Organizer->gameFeatures()));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(std::make_shared<CreationGamePlugins>(moInfo));
+ registerFeature(std::make_shared<Fallout4UnmangedMods>(this));
+ registerFeature(std::make_shared<Fallout4BSAInvalidation>(dataArchives.get(), this));
+
+ return true;
+}
+
+QString GameFallout4::gameName() const
+{
+ return "Fallout 4";
+}
+
+void GameFallout4::detectGame()
+{
+ m_GamePath = identifyGamePath();
+ m_MyGamesPath = determineMyGamesPath("Fallout4");
+}
+
+QList<ExecutableInfo> GameFallout4::executables() const
+{
+ return QList<ExecutableInfo>()
+ << ExecutableInfo("F4SE",
+ findInGameFolder(m_Organizer->gameFeatures()
+ ->gameFeature<MOBase::ScriptExtender>()
+ ->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<ExecutableForcedLoadSetting> GameFallout4::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+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<PluginSetting> GameFallout4::settings() const
+{
+ return QList<PluginSetting>();
+}
+
+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<const GamebryoSaveGame>
+GameFallout4::makeSaveGame(QString filePath) const
+{
+ return std::make_shared<const Fallout4SaveGame>(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<unsigned int> GameFallout4::activeProblems() const
+{
+ std::vector<unsigned int> 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("<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>");
+ }
+ }
+ 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 <QObject>
+#include <QtGlobal>
+
+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<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ 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<unsigned int> 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<MOBase::PluginSetting> settings() const override;
+ virtual MappingType mappings() const override;
+
+protected:
+ std::shared_ptr<const GamebryoSaveGame> 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
--- /dev/null
+++ b/libs/game_bethesda/src/games/fallout4/splash.png
Binary files 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 @@
+<RCC>
+ <qresource prefix="/Fallout4London">
+ <file alias="splash">splash.png</file>
+ </qresource>
+</RCC>
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 <imoinfo.h>
+#include <utility.h>
+
+#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 <bsainvalidation.h>
+#include <dataarchives.h>
+
+#include <memory>
+
+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 <utility.h>
+
+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 <QDir>
+#include <QStringList>
+
+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 <gamebryomoddatachecker.h>
+
+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 <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+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<Content> getAllContents() const override
+ {
+ auto contents = GamebryoModDataContent::getAllContents();
+ contents.push_back(
+ Content(CONTENT_MATERIAL, "Materials", ":/MO/gui/content/material"));
+ return contents;
+ }
+
+ std::vector<int>
+ getContentsFor(std::shared_ptr<const MOBase::IFileTree> 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 <Windows.h>
+#else
+#include <QDateTime>
+
+using SYSTEMTIME = _SYSTEMTIME;
+
+static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st)
+{
+ const uint64_t ticks = (static_cast<uint64_t>(ft->dwHighDateTime) << 32) |
+ static_cast<uint64_t>(ft->dwLowDateTime);
+ const int64_t unixSecs = static_cast<int64_t>(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<uint16_t>(d.year());
+ st->wMonth = static_cast<uint16_t>(d.month());
+ st->wDayOfWeek = static_cast<uint16_t>(d.dayOfWeek() % 7);
+ st->wDay = static_cast<uint16_t>(d.day());
+ st->wHour = static_cast<uint16_t>(t.hour());
+ st->wMinute = static_cast<uint16_t>(t.minute());
+ st->wSecond = static_cast<uint16_t>(t.second());
+ st->wMilliseconds = static_cast<uint16_t>(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<uint32_t>(); // header size
+ file.skip<uint32_t>(); // header version
+ file.read(saveNumber);
+
+ file.read(playerName);
+
+ uint32_t temp;
+ file.read(temp);
+ playerLevel = static_cast<unsigned short>(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<unsigned short>(); // Player gender (0 = male)
+ file.skip<float>(2); // experience gathered, experience required
+
+ file.read(creationTime);
+}
+
+std::unique_ptr<GamebryoSaveGame::DataFields>
+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<DataFields> fields = std::make_unique<DataFields>();
+
+ fields->Screenshot = file.readImage(384, true);
+
+ uint8_t saveGameVersion = file.readChar();
+ file.read(ignore); // game version
+ file.skip<uint32_t>(); // 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<DataFields> 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 <QString>
+#include <QStringList>
+
+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 <gamegamebryo.h>
+
+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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameFallout4London</name>
+ <message>
+ <location filename="gamefo4london.cpp" line="104"/>
+ <source>Fallout 4 London Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamefo4london.cpp" line="114"/>
+ <source>Adds support for the game Fallout 4 London.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamefo4london.cpp" line="349"/>
+ <source>sTestFile entries are present</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamefo4london.cpp" line="357"/>
+ <source>&lt;p&gt;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.&lt;/p&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <creationgameplugins.h>
+#include <executableinfo.h>
+#include <gamebryolocalsavegames.h>
+#include <gamebryosavegameinfo.h>
+#include <pluginsetting.h>
+
+#include <QCoreApplication>
+#include <QDir>
+#include <QFileInfo>
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <QStringList>
+
+#include <memory>
+
+#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<Fallout4LondonDataArchives>(this);
+
+ registerFeature(std::make_shared<Fallout4LondonScriptExtender>(this));
+ registerFeature(dataArchives);
+ registerFeature(
+ std::make_shared<GamebryoLocalSavegames>(this, "fo4londoncustom.ini"));
+ registerFeature(std::make_shared<Fallout4LondonModDataChecker>(this));
+ registerFeature(
+ std::make_shared<Fallout4LondonModDataContent>(m_Organizer->gameFeatures()));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(std::make_shared<CreationGamePlugins>(moInfo));
+ registerFeature(std::make_shared<Fallout4LondonUnmangedMods>(this));
+ registerFeature(
+ std::make_shared<Fallout4LondonBSAInvalidation>(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<ExecutableInfo> GameFallout4London::executables() const
+{
+ return QList<ExecutableInfo>()
+ << ExecutableInfo("F4SE",
+ findInGameFolder(m_Organizer->gameFeatures()
+ ->gameFeature<MOBase::ScriptExtender>()
+ ->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<ExecutableForcedLoadSetting> GameFallout4London::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+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<PluginSetting> GameFallout4London::settings() const
+{
+ return QList<PluginSetting>();
+}
+
+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<const GamebryoSaveGame>
+GameFallout4London::makeSaveGame(QString filePath) const
+{
+ return std::make_shared<const Fallout4LondonSaveGame>(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<unsigned int> GameFallout4London::activeProblems() const
+{
+ std::vector<unsigned int> 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("<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>");
+ }
+ }
+ 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 <QObject>
+#include <QtGlobal>
+
+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<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ 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<unsigned int> 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<MOBase::PluginSetting> settings() const override;
+ virtual MappingType mappings() const override;
+
+protected:
+ std::shared_ptr<const GamebryoSaveGame> 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
--- /dev/null
+++ b/libs/game_bethesda/src/games/fallout4london/splash.png
Binary files 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 @@
+<RCC>
+ <qresource prefix="/Fallout4VR">
+ <file alias="splash">splash.png</file>
+ </qresource>
+</RCC>
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 <utility.h>
+
+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 <QDir>
+#include <QStringList>
+
+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 <creationgameplugins.h>
+
+#include <QObject>
+#include <QtGlobal>
+
+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 <gamebryomoddatachecker.h>
+
+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 <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+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<Content> getAllContents() const override
+ {
+ auto contents = GamebryoModDataContent::getAllContents();
+ contents.push_back(
+ Content(CONTENT_MATERIAL, "Materials", ":/MO/gui/content/material"));
+ return contents;
+ }
+
+ std::vector<int>
+ getContentsFor(std::shared_ptr<const MOBase::IFileTree> 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 <Windows.h>
+#else
+#include <QDateTime>
+
+using SYSTEMTIME = _SYSTEMTIME;
+
+static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st)
+{
+ const uint64_t ticks = (static_cast<uint64_t>(ft->dwHighDateTime) << 32) |
+ static_cast<uint64_t>(ft->dwLowDateTime);
+ const int64_t unixSecs = static_cast<int64_t>(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<uint16_t>(d.year());
+ st->wMonth = static_cast<uint16_t>(d.month());
+ st->wDayOfWeek = static_cast<uint16_t>(d.dayOfWeek() % 7);
+ st->wDay = static_cast<uint16_t>(d.day());
+ st->wHour = static_cast<uint16_t>(t.hour());
+ st->wMinute = static_cast<uint16_t>(t.minute());
+ st->wSecond = static_cast<uint16_t>(t.second());
+ st->wMilliseconds = static_cast<uint16_t>(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<uint32_t>(); // header size
+ file.skip<uint32_t>(); // header version
+ file.read(saveNumber);
+
+ file.read(playerName);
+
+ uint32_t temp;
+ file.read(temp);
+ playerLevel = static_cast<unsigned short>(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<unsigned short>(); // Player gender (0 = male)
+ file.skip<float>(2); // experience gathered, experience required
+
+ file.read(creationTime);
+}
+
+std::unique_ptr<GamebryoSaveGame::DataFields>
+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<DataFields> fields = std::make_unique<DataFields>();
+
+ fields->Screenshot = file.readImage(384, true);
+
+ uint8_t saveGameVersion = file.readChar();
+ file.read(ignore); // game version
+ file.skip<uint32_t>(); // 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<DataFields> 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 <QString>
+#include <QStringList>
+
+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 <gamegamebryo.h>
+
+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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameFallout4VR</name>
+ <message>
+ <location filename="gamefallout4vr.cpp" line="82"/>
+ <source>Fallout 4 VR Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamefallout4vr.cpp" line="92"/>
+ <source>Adds support for the game Fallout 4 VR.
+Splash by %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <executableinfo.h>
+#include <gamebryolocalsavegames.h>
+#include <gamebryosavegameinfo.h>
+#include <pluginsetting.h>
+
+#include <QCoreApplication>
+#include <QDir>
+#include <QFileInfo>
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <QStringList>
+
+#include <memory>
+
+#include "scopeguard.h"
+
+using namespace MOBase;
+
+GameFallout4VR::GameFallout4VR() {}
+
+bool GameFallout4VR::init(IOrganizer* moInfo)
+{
+ if (!GameGamebryo::init(moInfo)) {
+ return false;
+ }
+
+ registerFeature(std::make_shared<Fallout4VRDataArchives>(this));
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "fallout4custom.ini"));
+ registerFeature(std::make_shared<Fallout4VRModDataChecker>(this));
+ registerFeature(
+ std::make_shared<Fallout4VRModDataContent>(m_Organizer->gameFeatures()));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(std::make_shared<Fallout4VRGamePlugins>(moInfo));
+ registerFeature(std::make_shared<Fallout4VRUnmangedMods>(this));
+
+ return true;
+}
+
+QString GameFallout4VR::gameName() const
+{
+ return "Fallout 4 VR";
+}
+
+void GameFallout4VR::detectGame()
+{
+ m_GamePath = identifyGamePath();
+ m_MyGamesPath = determineMyGamesPath("Fallout4VR");
+}
+
+QList<ExecutableInfo> GameFallout4VR::executables() const
+{
+ return QList<ExecutableInfo>()
+ << ExecutableInfo("Fallout 4 VR", findInGameFolder(binaryName()))
+ << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe"))
+ << ExecutableInfo("LOOT", QFileInfo(getLootPath()))
+ .withArgument("--game=\"Fallout4VR\"");
+}
+
+QList<ExecutableForcedLoadSetting> GameFallout4VR::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+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<PluginSetting> GameFallout4VR::settings() const
+{
+ return QList<PluginSetting>();
+}
+
+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<const GamebryoSaveGame>
+GameFallout4VR::makeSaveGame(QString filePath) const
+{
+ return std::make_shared<const Fallout4VRSaveGame>(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 <QObject>
+#include <QtGlobal>
+
+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<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ 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<MOBase::PluginSetting> settings() const override;
+
+protected:
+ std::shared_ptr<const GamebryoSaveGame> 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
--- /dev/null
+++ b/libs/game_bethesda/src/games/fallout4vr/splash.png
Binary files 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 @@
+<RCC>
+ <qresource prefix="/Fallout76">
+ <file alias="splash">splash.png</file>
+ </qresource>
+</RCC>
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 <utility.h>
+
+#include <QRegularExpression>
+
+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 <QDir>
+#include <QStringList>
+
+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 <gamebryomoddatachecker.h>
+
+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 <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+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<Content> getAllContents() const override
+ {
+ auto contents = GamebryoModDataContent::getAllContents();
+ contents.push_back(
+ Content(CONTENT_MATERIAL, "Materials", ":/MO/gui/content/material"));
+ return contents;
+ }
+
+ std::vector<int>
+ getContentsFor(std::shared_ptr<const MOBase::IFileTree> 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 <Windows.h>
+#else
+#include <QDateTime>
+
+using SYSTEMTIME = _SYSTEMTIME;
+
+static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st)
+{
+ const uint64_t ticks = (static_cast<uint64_t>(ft->dwHighDateTime) << 32) |
+ static_cast<uint64_t>(ft->dwLowDateTime);
+ const int64_t unixSecs = static_cast<int64_t>(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<uint16_t>(d.year());
+ st->wMonth = static_cast<uint16_t>(d.month());
+ st->wDayOfWeek = static_cast<uint16_t>(d.dayOfWeek() % 7);
+ st->wDay = static_cast<uint16_t>(d.day());
+ st->wHour = static_cast<uint16_t>(t.hour());
+ st->wMinute = static_cast<uint16_t>(t.minute());
+ st->wSecond = static_cast<uint16_t>(t.second());
+ st->wMilliseconds = static_cast<uint16_t>(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<unsigned long>(); // header size
+ file.skip<uint32_t>(); // header version
+ file.read(saveNumber);
+
+ file.read(playerName);
+
+ unsigned long temp;
+ file.read(temp);
+ playerLevel = static_cast<unsigned short>(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<unsigned short>(); // Player gender (0 = male)
+ file.skip<float>(2); // experience gathered, experience required
+
+ FILETIME ftime;
+ file.read(ftime);
+}
+
+std::unique_ptr<GamebryoSaveGame::DataFields> 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<DataFields> fields = std::make_unique<DataFields>();
+
+ file.readImage(384, true);
+
+ uint8_t saveGameVersion = file.readChar();
+ QString ignore;
+ file.read(ignore); // game version
+ file.skip<uint32_t>(); // 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<DataFields> 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 <QString>
+#include <QStringList>
+
+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 <gamegamebryo.h>
+
+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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameFallout76</name>
+ <message>
+ <location filename="gamefallout76.cpp" line="79"/>
+ <source>Adds support for the game Fallout 76.
+Splash by %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <creationgameplugins.h>
+#include <executableinfo.h>
+#include <pluginsetting.h>
+
+#include <QCoreApplication>
+#include <QDir>
+#include <QFileInfo>
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <QStringList>
+
+#include <memory>
+
+#include "scopeguard.h"
+
+using namespace MOBase;
+
+GameFallout76::GameFallout76() {}
+
+bool GameFallout76::init(IOrganizer* moInfo)
+{
+ if (!GameGamebryo::init(moInfo)) {
+ return false;
+ }
+
+ registerFeature(std::make_shared<Fallout76DataArchives>(this));
+ registerFeature(std::make_shared<Fallout76ModDataChecker>(this));
+ registerFeature(
+ std::make_shared<Fallout76ModDataContent>(m_Organizer->gameFeatures()));
+ registerFeature(std::make_shared<CreationGamePlugins>(moInfo));
+ registerFeature(std::make_shared<Fallout76UnmangedMods>(this));
+
+ return true;
+}
+
+QString GameFallout76::gameName() const
+{
+ return "Fallout 76";
+}
+
+void GameFallout76::detectGame()
+{
+ m_GamePath = identifyGamePath();
+ m_MyGamesPath = determineMyGamesPath(gameName());
+}
+
+QList<ExecutableInfo> GameFallout76::executables() const
+{
+ return QList<ExecutableInfo>()
+ << ExecutableInfo("Fallout 76", findInGameFolder(binaryName()));
+}
+
+QList<ExecutableForcedLoadSetting> 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<PluginSetting> GameFallout76::settings() const
+{
+ return QList<PluginSetting>();
+}
+
+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<std::shared_ptr<const MOBase::ISaveGame>>
+GameFallout76::listSaves(QDir folder) const
+{
+ return {};
+}
+
+QString GameFallout76::identifyGamePath() const
+{
+ return parseSteamLocation(steamAPPId(), gameShortName());
+}
+
+std::shared_ptr<const GamebryoSaveGame> 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 <QObject>
+#include <QtGlobal>
+
+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<MOBase::ExecutableInfo> executables() const override;
+ QList<MOBase::ExecutableForcedLoadSetting> 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<std::shared_ptr<const MOBase::ISaveGame>>
+ 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<MOBase::PluginSetting> settings() const override;
+
+protected:
+ QString identifyGamePath() const override;
+ std::shared_ptr<const GamebryoSaveGame> 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
--- /dev/null
+++ b/libs/game_bethesda/src/games/fallout76/splash.png
Binary files 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
+ "$<TARGET_FILE:game_falloutnv>"
+ "${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 <memory>
+
+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 <utility.h>
+
+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 <QDir>
+#include <QString>
+#include <QStringList>
+#include <gamebryodataarchives.h>
+#include <iprofile.h>
+
+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 <gamebryomoddatachecker.h>
+
+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 <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+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<uint32_t>(); // Save header size
+
+ file.skip<uint32_t>(); // File version?
+ file.skip<unsigned char>(); // 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<GamebryoSaveGame::DataFields> FalloutNVSaveGame::fetchDataFields() const
+{
+ FileWrapper file(getFilepath(), "FO3SAVEGAME");
+
+ std::unique_ptr<DataFields> fields = std::make_unique<DataFields>();
+
+ 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<char>(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<DataFields> 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 <QString>
+#include <QStringList>
+
+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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameFalloutNV</name>
+ <message>
+ <location filename="gamefalloutnv.cpp" line="181"/>
+ <source>Fallout NV Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamefalloutnv.cpp" line="191"/>
+ <source>Adds support for the game Fallout New Vegas</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamefalloutnv.cpp" line="203"/>
+ <source>While not recommended by the FNV modding community, enables LOOT sorting</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <gamebryogameplugins.h>
+#include <gamebryolocalsavegames.h>
+#include <gamebryosavegameinfo.h>
+#include <gamebryounmanagedmods.h>
+
+#include <QCoreApplication>
+#include <QDir>
+#include <QFileInfo>
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <QStringList>
+
+#include <memory>
+
+using namespace MOBase;
+
+GameFalloutNV::GameFalloutNV() {}
+
+bool GameFalloutNV::init(IOrganizer* moInfo)
+{
+ if (!GameGamebryo::init(moInfo)) {
+ return false;
+ }
+
+ auto dataArchives = std::make_shared<FalloutNVDataArchives>(this);
+ registerFeature(std::make_shared<FalloutNVScriptExtender>(this));
+ registerFeature(dataArchives);
+ registerFeature(std::make_shared<FalloutNVBSAInvalidation>(dataArchives.get(), this));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "fallout.ini"));
+ registerFeature(std::make_shared<FalloutNVModDataChecker>(this));
+ registerFeature(
+ std::make_shared<FalloutNVModDataContent>(m_Organizer->gameFeatures()));
+ registerFeature(std::make_shared<GamebryoGamePlugins>(moInfo));
+ registerFeature(std::make_shared<GamebryoUnmangedMods>(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<ExecutableInfo> GameFalloutNV::executables() const
+{
+ ExecutableInfo game("New Vegas", findInGameFolder(binaryName()));
+ ExecutableInfo launcher("Fallout Launcher", findInGameFolder(getLauncherName()));
+ QList<ExecutableInfo> extraExecutables =
+ QList<ExecutableInfo>() << 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<MOBase::ScriptExtender>()
+ ->loaderName())));
+ } else {
+ game.withArgument("-EpicPortal");
+ launcher.withArgument("-EpicPortal");
+ }
+ QList<ExecutableInfo> executables = {game, launcher};
+ executables += extraExecutables;
+ return executables;
+}
+
+QList<ExecutableForcedLoadSetting> GameFalloutNV::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+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<PluginSetting> GameFalloutNV::settings() const
+{
+ return QList<PluginSetting>()
+ << 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<const GamebryoSaveGame>
+GameFalloutNV::makeSaveGame(QString filePath) const
+{
+ return std::make_shared<const FalloutNVSaveGame>(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 <QObject>
+#include <QtGlobal>
+
+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<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ 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<MOBase::PluginSetting> 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<const GamebryoSaveGame> 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameMorrowind</name>
+ <message>
+ <location filename="gamemorrowind.cpp" line="106"/>
+ <source>Morrowind Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamemorrowind.cpp" line="116"/>
+ <source>Adds support for the game Morrowind.
+Splash by %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MorrowindSaveGameInfoWidget</name>
+ <message>
+ <location filename="morrowindsavegameinfowidget.ui" line="53"/>
+ <source>Save #</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="morrowindsavegameinfowidget.ui" line="78"/>
+ <source>Character</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="morrowindsavegameinfowidget.ui" line="103"/>
+ <source>Level</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="morrowindsavegameinfowidget.ui" line="128"/>
+ <source>Health</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="morrowindsavegameinfowidget.ui" line="153"/>
+ <source>Location</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="morrowindsavegameinfowidget.ui" line="178"/>
+ <source>Date</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="morrowindsavegameinfowidget.cpp" line="85"/>
+ <source>Missing ESPs</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="morrowindsavegameinfowidget.cpp" line="118"/>
+ <source>None</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>QObject</name>
+ <message>
+ <location filename="morrowindgameplugins.cpp" line="120"/>
+ <source>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.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <gamebryounmanagedmods.h>
+
+#include <QCoreApplication>
+#include <QDebug>
+#include <QFileInfo>
+
+#include <QtDebug>
+
+#include <exception>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+using namespace MOBase;
+
+GameMorrowind::GameMorrowind() {}
+
+bool GameMorrowind::init(IOrganizer* moInfo)
+{
+ if (!GameGamebryo::init(moInfo)) {
+ return false;
+ }
+
+ auto dataArchives = std::make_shared<MorrowindDataArchives>(this);
+ registerFeature(dataArchives);
+ registerFeature(std::make_shared<MorrowindBSAInvalidation>(dataArchives.get(), this));
+ registerFeature(std::make_shared<MorrowindSaveGameInfo>(this));
+ registerFeature(std::make_shared<MorrowindLocalSavegames>(this));
+ registerFeature(std::make_shared<MorrowindModDataChecker>(this));
+ registerFeature(
+ std::make_shared<MorrowindModDataContent>(m_Organizer->gameFeatures()));
+ registerFeature(std::make_shared<MorrowindGamePlugins>(moInfo));
+ registerFeature(std::make_shared<GamebryoUnmangedMods>(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<ExecutableInfo> GameMorrowind::executables() const
+{
+ return QList<ExecutableInfo>()
+ << 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<ExecutableForcedLoadSetting> GameMorrowind::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+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<PluginSetting> GameMorrowind::settings() const
+{
+ return QList<PluginSetting>();
+}
+
+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<const GamebryoSaveGame>
+GameMorrowind::makeSaveGame(QString filePath) const
+{
+ return std::make_shared<const MorrowindSaveGame>(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 <QObject>
+#include <QtGlobal>
+
+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<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ 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<MOBase::PluginSetting> settings() const override;
+
+protected:
+ virtual QString savegameExtension() const override;
+ virtual QString savegameSEExtension() const override;
+ virtual std::shared_ptr<const GamebryoSaveGame>
+ 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 @@
+<RCC>
+ <qresource prefix="/Morrowind">
+ <file alias="splash">splash.png</file>
+ </qresource>
+</RCC>
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 <memory>
+
+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 <utility.h>
+
+#include <QSettings>
+
+#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 <QDir>
+#include <QString>
+#include <QStringList>
+#include <gamebryodataarchives.h>
+#include <iplugingame.h>
+#include <iprofile.h>
+
+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 <imodinterface.h>
+#include <iplugingame.h>
+#include <ipluginlist.h>
+#include <report.h>
+#include <scopeguard.h>
+#include <utility.h>
+
+#include <QDateTime>
+#include <QDir>
+#include <QSettings>
+#include <QString>
+#include <QStringEncoder>
+#include <QStringList>
+
+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 <gamebryogameplugins.h>
+
+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 <QtDebug>
+#include <iprofile.h>
+#include <stddef.h>
+#include <string>
+
+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 <localsavegames.h>
+
+#include "iplugingame.h"
+#include <QDir>
+#include <QString>
+
+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 <gamebryomoddatachecker.h>
+
+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 <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+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 <QPixmap>
+#include <QRegularExpression>
+#include <filesystem>
+
+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<MorrowindDataFields*>(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<uint32_t>(3); // data size
+ file.skip<unsigned char>(4); // HEDR tag
+ file.skip<uint32_t>(); // header size
+ file.skip<float>(); // header version
+ file.skip<uint32_t>(); // following data chunk size? seems to be 9 groupings of 32
+ // bytes
+ file.skip<unsigned char>(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<char> saveNameBuffer(256); // 31 char save name with a null terminator
+ file.read(saveNameBuffer.data(), 256);
+ saveName = QString::fromLatin1(saveNameBuffer.data(), -1).trimmed();
+ }
+
+ file.skip<uint32_t>(); // NumRecords (for the entire save)
+ std::vector<char> 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<unsigned char>(4); // DATA record
+
+ // Typically size 8 - contains length of master data for version checking
+ file.read(len); // Length
+ file.skip<unsigned char>(len);
+
+ file.read(buffer.data(), 4); // Get next record type
+ plugins.push_back(name);
+ }
+
+ // Start of GMDT
+ file.skip<uint32_t>(); // size of record
+
+ file.read(playerCurrentHealth);
+ file.read(playerMaxHealth);
+
+ file.skip<double>(); // current stam?
+ file.skip<double>(); // max stam?
+ // file.skip<double>(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<GamebryoSaveGame::DataFields> MorrowindSaveGame::fetchDataFields() const
+{
+ FileWrapper file(getFilepath(), "TES3");
+ std::vector<char> buffer(255);
+
+ std::unique_ptr<MorrowindDataFields> fields = std::make_unique<MorrowindDataFields>();
+
+ {
+ QString dummy;
+ float dummyF;
+ fetchInformationFields(file, dummy, fields->Plugins, dummyF, dummyF, dummy, dummyF,
+ dummy);
+ }
+
+ file.skip<unsigned char>(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<unsigned char>(4); // SCRS
+ file.skip<uint32_t>(); // 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<unsigned char>();
+ std::vector<char> buff(4);
+ file.read(buff.data(), 4);
+ while (QString::fromLatin1(buff.data(), 4) != "NPC_") {
+ uint32_t len;
+ file.read(len);
+ file.skip<unsigned char>(8 + len);
+ file.read(buff.data(), 4);
+ }
+ while (QString::fromLatin1(buff.data(), 4) == "NPC_") {
+ uint32_t size;
+ file.read(size);
+ file.skip<unsigned long>(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<unsigned char>(len);
+ file.read(buff.data(), 4);
+ }
+ file.skip<unsigned long>();
+ file.read(fields->PCLevel);
+ } else {
+ file.skip<unsigned char>(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<DataFields> 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<GameMorrowind const*>(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 <QDate>
+#include <QDateTime>
+#include <QFont>
+#include <QFrame>
+#include <QLabel>
+#include <QLayout>
+#include <QLayoutItem>
+#include <QPixmap>
+#include <QString>
+#include <QStyle>
+#include <QTime>
+#include <QVBoxLayout>
+
+#include <Qt>
+#include <QtGlobal>
+
+#include <memory>
+
+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<MorrowindSaveGame const&>(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 <QObject>
+
+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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MorrowindSaveGameInfoWidget</class>
+ <widget class="QWidget" name="MorrowindSaveGameInfoWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>300</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string/>
+ </property>
+ <property name="toolTip">
+ <string notr="true"/>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="saveNameLabel">
+ <property name="font">
+ <font>
+ <pointsize>10</pointsize>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="fieldGrowthPolicy">
+ <enum>QFormLayout::AllNonFixedFieldsGrow</enum>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Save #</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="saveNumLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Character</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="characterLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Level</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="levelLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Health</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLabel" name="healthLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0">
+ <widget class="QLabel" name="label_5">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Location</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="1">
+ <widget class="QLabel" name="locationLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0">
+ <widget class="QLabel" name="label_6">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Date</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
+ <widget class="QLabel" name="dateLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QFrame" name="gameFrame">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="screenshotLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
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
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/splash.png
Binary files 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameNehrim</name>
+ <message>
+ <location filename="gamenehrim.cpp" line="84"/>
+ <source>Nehrim Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamenehrim.cpp" line="94"/>
+ <source>Adds support for the game Nehrim</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <gamebryogameplugins.h>
+#include <gamebryolocalsavegames.h>
+#include <gamebryosavegameinfo.h>
+#include <gamebryounmanagedmods.h>
+
+#include <QCoreApplication>
+#include <QDir>
+#include <QFileInfo>
+
+#include <memory>
+
+using namespace MOBase;
+
+GameNehrim::GameNehrim() {}
+
+bool GameNehrim::init(IOrganizer* moInfo)
+{
+ if (!GameGamebryo::init(moInfo)) {
+ return false;
+ }
+
+ auto dataArchives = std::make_shared<NehrimDataArchives>(this);
+ registerFeature(std::make_shared<NehrimScriptExtender>(this));
+ registerFeature(dataArchives);
+ registerFeature(std::make_shared<NehrimBSAInvalidation>(dataArchives.get(), this));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "oblivion.ini"));
+ registerFeature(std::make_shared<NehrimModDataChecker>(this));
+ registerFeature(std::make_shared<NehrimModDataContent>(m_Organizer->gameFeatures()));
+ registerFeature(std::make_shared<GamebryoGamePlugins>(moInfo));
+ registerFeature(std::make_shared<GamebryoUnmangedMods>(this));
+ return true;
+}
+
+QString GameNehrim::gameName() const
+{
+ return "Nehrim";
+}
+
+QList<ExecutableInfo> GameNehrim::executables() const
+{
+ return QList<ExecutableInfo>()
+ << 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<ExecutableForcedLoadSetting> GameNehrim::executableForcedLoads() const
+{
+ // TODO Search game directory for OBSE DLLs
+ return QList<ExecutableForcedLoadSetting>()
+ << 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<PluginSetting> GameNehrim::settings() const
+{
+ return QList<PluginSetting>();
+}
+
+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<const GamebryoSaveGame> GameNehrim::makeSaveGame(QString filePath) const
+{
+ return std::make_shared<const NehrimSaveGame>(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 <QObject>
+#include <QtGlobal>
+
+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<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ 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<MOBase::PluginSetting> settings() const override;
+
+protected:
+ std::shared_ptr<const GamebryoSaveGame> 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 <memory>
+
+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 <utility.h>
+
+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 <QDir>
+#include <QString>
+#include <QStringList>
+#include <gamebryodataarchives.h>
+#include <iprofile.h>
+
+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<const MOBase::IFileTree> 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<MOBase::IFileTree>
+NehrimModDataChecker::fix(std::shared_ptr<MOBase::IFileTree> 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 <gamebryomoddatachecker.h>
+
+class NehrimModDataChecker : public GamebryoModDataChecker
+{
+public:
+ using GamebryoModDataChecker::GamebryoModDataChecker;
+
+ CheckReturn
+ dataLooksValid(std::shared_ptr<const MOBase::IFileTree> fileTree) const override;
+ std::shared_ptr<MOBase::IFileTree>
+ fix(std::shared_ptr<MOBase::IFileTree> 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 <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+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<unsigned char>(); // Major version
+ file.skip<unsigned char>(); // Minor version
+
+ file.skip<_SYSTEMTIME>(); // exe last modified (!)
+
+ file.skip<uint32_t>(); // Header version
+ file.skip<uint32_t>(); // Header size
+
+ file.read(saveNumber);
+
+ file.read(playerName);
+ file.read(playerLevel);
+ file.read(playerLocation);
+
+ file.skip<float>(); // game days
+ file.skip<uint32_t>(); // 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<GamebryoSaveGame::DataFields> NehrimSaveGame::fetchDataFields() const
+{
+ FileWrapper file(getFilepath(), "TES4SAVEGAME");
+ file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING);
+
+ std::unique_ptr<DataFields> fields = std::make_unique<DataFields>();
+
+ {
+ 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<uint32_t>(); // 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<DataFields> 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 <QString>
+#include <QStringList>
+
+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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameOblivion</name>
+ <message>
+ <location filename="gameoblivion.cpp" line="84"/>
+ <source>Oblivion Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gameoblivion.cpp" line="94"/>
+ <source>Adds support for the game Oblivion</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <gamebryogameplugins.h>
+#include <gamebryolocalsavegames.h>
+#include <gamebryosavegameinfo.h>
+#include <gamebryounmanagedmods.h>
+
+#include <QCoreApplication>
+#include <QDir>
+#include <QFileInfo>
+
+#include <memory>
+
+using namespace MOBase;
+
+GameOblivion::GameOblivion() {}
+
+bool GameOblivion::init(IOrganizer* moInfo)
+{
+ if (!GameGamebryo::init(moInfo)) {
+ return false;
+ }
+
+ auto dataArchives = std::make_shared<OblivionDataArchives>(this);
+ registerFeature(std::make_shared<OblivionScriptExtender>(this));
+ registerFeature(dataArchives);
+ registerFeature(std::make_shared<OblivionBSAInvalidation>(dataArchives.get(), this));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "oblivion.ini"));
+ registerFeature(std::make_shared<OblivionModDataChecker>(this));
+ registerFeature(
+ std::make_shared<OblivionModDataContent>(m_Organizer->gameFeatures()));
+ registerFeature(std::make_shared<GamebryoGamePlugins>(moInfo));
+ registerFeature(std::make_shared<GamebryoUnmangedMods>(this));
+ return true;
+}
+
+QString GameOblivion::gameName() const
+{
+ return "Oblivion";
+}
+
+QList<ExecutableInfo> GameOblivion::executables() const
+{
+ return QList<ExecutableInfo>()
+ << 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<ExecutableForcedLoadSetting> GameOblivion::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>()
+ << 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<PluginSetting> 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<const GamebryoSaveGame>
+GameOblivion::makeSaveGame(QString filePath) const
+{
+ return std::make_shared<const OblivionSaveGame>(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 <QObject>
+#include <QtGlobal>
+
+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<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ 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<MOBase::PluginSetting> settings() const override;
+
+protected:
+ QString identifyGamePath() const override;
+ std::shared_ptr<const GamebryoSaveGame> 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 <memory>
+
+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 <utility.h>
+
+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 <QDir>
+#include <QString>
+#include <QStringList>
+#include <gamebryodataarchives.h>
+#include <iprofile.h>
+
+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<const MOBase::IFileTree> 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<MOBase::IFileTree>
+OblivionModDataChecker::fix(std::shared_ptr<MOBase::IFileTree> 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 <gamebryomoddatachecker.h>
+
+class OblivionModDataChecker : public GamebryoModDataChecker
+{
+public:
+ using GamebryoModDataChecker::GamebryoModDataChecker;
+
+ CheckReturn
+ dataLooksValid(std::shared_ptr<const MOBase::IFileTree> fileTree) const override;
+ std::shared_ptr<MOBase::IFileTree>
+ fix(std::shared_ptr<MOBase::IFileTree> 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 <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+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<unsigned char>(); // Major version
+ file.skip<unsigned char>(); // Minor version
+
+ file.skip<_SYSTEMTIME>(); // exe last modified (!)
+
+ file.skip<uint32_t>(); // Header version
+ file.skip<uint32_t>(); // Header size
+
+ file.read(saveNumber);
+
+ file.read(playerName);
+ file.read(playerLevel);
+ file.read(playerLocation);
+
+ file.skip<float>(); // game days
+ file.skip<uint32_t>(); // 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<GamebryoSaveGame::DataFields> OblivionSaveGame::fetchDataFields() const
+{
+ FileWrapper file(getFilepath(), "TES4SAVEGAME");
+ file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING);
+
+ std::unique_ptr<DataFields> fields = std::make_unique<DataFields>();
+
+ {
+ 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<DataFields> 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 <QString>
+#include <QStringList>
+
+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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameSkyrim</name>
+ <message>
+ <location filename="gameskyrim.cpp" line="92"/>
+ <source>Skyrim Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gameskyrim.cpp" line="102"/>
+ <source>Adds support for the game Skyrim</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <gamebryogameplugins.h>
+#include <gamebryolocalsavegames.h>
+#include <gamebryosavegameinfo.h>
+#include <gamebryounmanagedmods.h>
+
+#include <QCoreApplication>
+#include <QDebug>
+#include <QFileInfo>
+
+#include <QtDebug>
+
+#ifdef _WIN32
+#include <Windows.h>
+#include <winver.h>
+#endif
+
+#include <exception>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+using namespace MOBase;
+
+GameSkyrim::GameSkyrim() {}
+
+bool GameSkyrim::init(IOrganizer* moInfo)
+{
+ if (!GameGamebryo::init(moInfo)) {
+ return false;
+ }
+
+ auto dataArchives = std::make_shared<SkyrimDataArchives>(this);
+ registerFeature(std::make_shared<SkyrimScriptExtender>(this));
+ registerFeature(dataArchives);
+ registerFeature(std::make_shared<SkyrimBSAInvalidation>(dataArchives.get(), this));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "skyrim.ini"));
+ registerFeature(std::make_shared<SkyrimModDataChecker>(this));
+ registerFeature(std::make_shared<SkyrimModDataContent>(m_Organizer->gameFeatures()));
+ registerFeature(std::make_shared<SkyrimGamePlugins>(moInfo));
+ registerFeature(std::make_shared<GamebryoUnmangedMods>(this));
+
+ return true;
+}
+
+QString GameSkyrim::gameName() const
+{
+ return "Skyrim";
+}
+
+QList<ExecutableInfo> GameSkyrim::executables() const
+{
+ return QList<ExecutableInfo>()
+ << ExecutableInfo("SKSE",
+ findInGameFolder(m_Organizer->gameFeatures()
+ ->gameFeature<MOBase::ScriptExtender>()
+ ->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<ExecutableForcedLoadSetting> GameSkyrim::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+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<PluginSetting> GameSkyrim::settings() const
+{
+ QList<PluginSetting> 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<const GamebryoSaveGame> GameSkyrim::makeSaveGame(QString filepath) const
+{
+ return std::make_shared<const SkyrimSaveGame>(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<char> 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<VS_FIXEDFILEINFO*>(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 <QObject>
+#include <QtGlobal>
+
+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<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ 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<MOBase::PluginSetting> settings() const override;
+
+protected:
+ virtual QString savegameExtension() const override;
+ virtual QString savegameSEExtension() const override;
+ virtual std::shared_ptr<const GamebryoSaveGame>
+ 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 <memory>
+
+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 <utility.h>
+
+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 <QDir>
+#include <QString>
+#include <QStringList>
+#include <gamebryodataarchives.h>
+#include <iprofile.h>
+
+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 <ipluginlist.h>
+#include <report.h>
+#include <safewritefile.h>
+#include <scopeguard.h>
+
+#include <QDir>
+#include <QStringEncoder>
+#include <QStringList>
+
+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 <gamebryogameplugins.h>
+#include <imoinfo.h>
+#include <iplugingame.h>
+#include <map>
+
+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<QString, QByteArray> 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 <gamebryomoddatachecker.h>
+
+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 <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+// 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 <Windows.h>
+#else
+#include <QDateTime>
+
+// 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<uint64_t>(ft->dwHighDateTime) << 32) |
+ static_cast<uint64_t>(ft->dwLowDateTime);
+ const int64_t unixSecs = static_cast<int64_t>(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<uint16_t>(d.year());
+ st->wMonth = static_cast<uint16_t>(d.month());
+ st->wDayOfWeek = static_cast<uint16_t>(d.dayOfWeek() % 7); // Qt Mon=1..Sun=7 -> Win Sun=0..Sat=6
+ st->wDay = static_cast<uint16_t>(d.day());
+ st->wHour = static_cast<uint16_t>(t.hour());
+ st->wMinute = static_cast<uint16_t>(t.minute());
+ st->wSecond = static_cast<uint16_t>(t.second());
+ st->wMilliseconds = static_cast<uint16_t>(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<uint32_t>(); // header size
+ file.skip<uint32_t>(); // header version
+ file.read(saveNumber);
+
+ file.read(playerName);
+
+ uint32_t temp;
+ file.read(temp);
+ playerLevel = static_cast<unsigned short>(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<unsigned short>(); // Player gender (0 = male)
+ file.skip<float>(2); // experience gathered, experience required
+
+ file.read(creationTime);
+}
+
+std::unique_ptr<GamebryoSaveGame::DataFields> SkyrimSaveGame::fetchDataFields() const
+{
+ FileWrapper file(getFilepath(), "TESV_SAVEGAME");
+ std::unique_ptr<DataFields> fields = std::make_unique<DataFields>();
+
+ {
+ QString dummyName, dummyLocation;
+ unsigned short dummyLevel;
+ uint32_t dummySaveNumber;
+ FILETIME dummyTime;
+
+ fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation,
+ dummyTime);
+ }
+
+ fields->Screenshot = file.readImage();
+
+ file.skip<unsigned char>(); // form version
+ file.skip<uint32_t>(); // 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<DataFields> 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 <QString>
+#include <QStringList>
+
+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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameSkyrimSE</name>
+ <message>
+ <location filename="gameskyrimse.cpp" line="180"/>
+ <source>Skyrim Special Edition Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gameskyrimse.cpp" line="190"/>
+ <source>Adds support for the game Skyrim Special Edition.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <creationgameplugins.h>
+#include <executableinfo.h>
+#include <gamebryolocalsavegames.h>
+#include <gamebryosavegameinfo.h>
+#include <pluginsetting.h>
+#include <utility.h>
+
+#include <QCoreApplication>
+#include <QDir>
+#include <QDirIterator>
+#include <QFileInfo>
+#include <QJsonDocument>
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <QStringList>
+
+#include "scopeguard.h"
+#include <memory>
+
+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<QString, QString> 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<SkyrimSEScriptExtender>(this));
+ registerFeature(std::make_shared<SkyrimSEDataArchives>(this));
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "Skyrimcustom.ini"));
+ registerFeature(std::make_shared<SkyrimSEModDataChecker>(this));
+ registerFeature(
+ std::make_shared<SkyrimSEModDataContent>(m_Organizer->gameFeatures()));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(std::make_shared<CreationGamePlugins>(moInfo));
+ registerFeature(std::make_shared<SkyrimSEUnmangedMods>(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<ExecutableInfo> GameSkyrimSE::executables() const
+{
+ return QList<ExecutableInfo>()
+ << ExecutableInfo("SKSE",
+ findInGameFolder(m_Organizer->gameFeatures()
+ ->gameFeature<MOBase::ScriptExtender>()
+ ->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<ExecutableForcedLoadSetting> GameSkyrimSE::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+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<PluginSetting> 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<const GamebryoSaveGame>
+GameSkyrimSE::makeSaveGame(QString filePath) const
+{
+ return std::make_shared<const SkyrimSESaveGame>(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<QString> 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 <QObject>
+#include <QtGlobal>
+
+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<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ 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<MOBase::PluginSetting> settings() const override;
+ virtual MappingType mappings() const override;
+
+protected:
+ std::shared_ptr<const GamebryoSaveGame> 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 <utility.h>
+
+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 <QDir>
+#include <QStringList>
+
+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 <gamebryomoddatachecker.h>
+
+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 <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+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 <Windows.h>
+#else
+#include <QDateTime>
+
+// 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<int64_t>(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<uint16_t>(d.year());
+ st->wMonth = static_cast<uint16_t>(d.month());
+ st->wDayOfWeek = static_cast<uint16_t>(d.dayOfWeek() % 7); // Qt Mon=1..Sun=7 → Win Sun=0..Sat=6
+ st->wDay = static_cast<uint16_t>(d.day());
+ st->wHour = static_cast<uint16_t>(t.hour());
+ st->wMinute = static_cast<uint16_t>(t.minute());
+ st->wSecond = static_cast<uint16_t>(t.second());
+ st->wMilliseconds = static_cast<uint16_t>(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<unsigned short>(temp);
+ file.read(playerLocation);
+
+ QString timeOfDay;
+ file.read(timeOfDay);
+
+ QString race;
+ file.read(race); // race name (i.e. BretonRace)
+
+ file.skip<unsigned short>(); // Player gender (0 = male)
+ file.skip<float>(2); // experience gathered, experience required
+
+ file.read(creationTime); // filetime
+}
+
+std::unique_ptr<GamebryoSaveGame::DataFields> 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<DataFields> fields = std::make_unique<DataFields>();
+
+ 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<DataFields> 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 <QString>
+#include <QStringList>
+
+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 <gamegamebryo.h>
+
+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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameSkyrimVR</name>
+ <message>
+ <location filename="gameskyrimvr.cpp" line="118"/>
+ <source>Skyrim VR Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gameskyrimvr.cpp" line="128"/>
+ <source>Adds support for the game Skyrim VR.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <executableinfo.h>
+#include <gamebryolocalsavegames.h>
+#include <gamebryosavegameinfo.h>
+#include <pluginsetting.h>
+
+#include <QCoreApplication>
+#include <QDir>
+#include <QFileInfo>
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <QStringList>
+
+#include "scopeguard.h"
+#include <memory>
+
+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<SkyrimVRScriptExtender>(this));
+ registerFeature(std::make_shared<SkyrimVRDataArchives>(this));
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "SkyrimVR.ini"));
+ registerFeature(std::make_shared<SkyrimVRModDataChecker>(this));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(
+ std::make_shared<SkyrimVRModDataContent>(m_Organizer->gameFeatures()));
+ registerFeature(std::make_shared<SkyrimVRGamePlugins>(moInfo));
+ registerFeature(std::make_shared<SkyrimVRUnmangedMods>(this));
+
+ return true;
+}
+
+QString GameSkyrimVR::gameName() const
+{
+ return "Skyrim VR";
+}
+
+QList<ExecutableInfo> GameSkyrimVR::executables() const
+{
+ return QList<ExecutableInfo>()
+ << ExecutableInfo("SKSE",
+ findInGameFolder(m_Organizer->gameFeatures()
+ ->gameFeature<MOBase::ScriptExtender>()
+ ->loaderName()))
+ << ExecutableInfo("Skyrim VR", findInGameFolder(binaryName()))
+ << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe"))
+ << ExecutableInfo("LOOT", QFileInfo(getLootPath()))
+ .withArgument("--game=\"Skyrim VR\"");
+}
+
+QList<ExecutableForcedLoadSetting> GameSkyrimVR::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+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<PluginSetting> 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<const GamebryoSaveGame>
+GameSkyrimVR::makeSaveGame(QString filePath) const
+{
+ return std::make_shared<const SkyrimVRSaveGame>(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 <QObject>
+#include <QtGlobal>
+
+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<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ 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<MOBase::PluginSetting> settings() const override;
+
+public: // IPluginFileMapper
+ virtual MappingType mappings() const override;
+
+protected:
+ std::shared_ptr<const GamebryoSaveGame> 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 <utility.h>
+
+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 <QDir>
+#include <QStringList>
+
+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 <creationgameplugins.h>
+
+#include <QObject>
+#include <QtGlobal>
+
+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 <gamebryomoddatachecker.h>
+
+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 <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+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 <Windows.h>
+#else
+#include <QDateTime>
+
+// 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<uint64_t>(ft->dwHighDateTime) << 32) |
+ static_cast<uint64_t>(ft->dwLowDateTime);
+ const int64_t unixSecs = static_cast<int64_t>(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<uint16_t>(d.year());
+ st->wMonth = static_cast<uint16_t>(d.month());
+ st->wDayOfWeek = static_cast<uint16_t>(d.dayOfWeek() % 7); // Qt Mon=1..Sun=7 -> Win Sun=0..Sat=6
+ st->wDay = static_cast<uint16_t>(d.day());
+ st->wHour = static_cast<uint16_t>(t.hour());
+ st->wMinute = static_cast<uint16_t>(t.minute());
+ st->wSecond = static_cast<uint16_t>(t.second());
+ st->wMilliseconds = static_cast<uint16_t>(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<unsigned short>(temp);
+
+ file.read(playerLocation);
+
+ QString timeOfDay;
+ file.read(timeOfDay);
+
+ QString race;
+ file.read(race); // race name (i.e. BretonRace)
+
+ file.skip<unsigned short>(); // Player gender (0 = male)
+ file.skip<float>(2); // experience gathered, experience required
+
+ file.read(creationTime); // filetime
+}
+
+std::unique_ptr<GamebryoSaveGame::DataFields> 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<DataFields> fields = std::make_unique<DataFields>();
+
+ 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<DataFields> 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 <QString>
+#include <QStringList>
+
+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 <gamegamebryo.h>
+
+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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameStarfield</name>
+ <message>
+ <location filename="gamestarfield.cpp" line="116"/>
+ <source>Starfield Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamestarfield.cpp" line="126"/>
+ <source>Adds support for the game Starfield.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamestarfield.cpp" line="139"/>
+ <source>Show a warning when ESP plugins are enabled in the load order.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamestarfield.cpp" line="142"/>
+ <source>Show a warning when plugins.txt management is invalid.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamestarfield.cpp" line="401"/>
+ <source>You have active ESP plugins in Starfield</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamestarfield.cpp" line="403"/>
+ <source>sTestFile entries are present</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamestarfield.cpp" line="413"/>
+ <source>&lt;p&gt;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&apos;s ability to load what it needs.&lt;/p&gt;&lt;p&gt;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&apos;re doing.&lt;/p&gt;&lt;p&gt;Notably, xEdit does not currently support saving ESP files.&lt;/p&gt;&lt;h4&gt;Current ESPs:&lt;/h4&gt;&lt;p&gt;%1&lt;/p&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamestarfield.cpp" line="427"/>
+ <source>&lt;p&gt;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.&lt;/p&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>QObject</name>
+ <message>
+ <location filename="starfieldunmanagedmods.cpp" line="70"/>
+ <source>ContentCatalog.txt appears to be corrupt: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>StarfieldModDataContent</name>
+ <message>
+ <location filename="starfieldmoddatacontent.h" line="27"/>
+ <source>Materials</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="starfieldmoddatacontent.h" line="28"/>
+ <source>Geometries</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="starfieldmoddatacontent.h" line="29"/>
+ <source>Video</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <executableinfo.h>
+#include <gamebryolocalsavegames.h>
+#include <gamebryosavegameinfo.h>
+#include <pluginsetting.h>
+
+#include <QCoreApplication>
+#include <QDesktopServices>
+#include <QDir>
+#include <QFileInfo>
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <QStringList>
+
+#include <memory>
+
+#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<StarfieldDataArchives>(this);
+ registerFeature(std::make_shared<StarfieldScriptExtender>(this));
+ registerFeature(dataArchives);
+ registerFeature(
+ std::make_shared<GamebryoLocalSavegames>(this, "StarfieldCustom.ini"));
+ registerFeature(std::make_shared<StarfieldModDataChecker>(this));
+ registerFeature(
+ std::make_shared<StarfieldModDataContent>(m_Organizer->gameFeatures()));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(std::make_shared<StarfieldGamePlugins>(moInfo));
+ registerFeature(std::make_shared<StarfieldUnmanagedMods>(this, localAppFolder()));
+ registerFeature(std::make_shared<StarfieldBSAInvalidation>(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<QString, QDir> GameStarfield::secondaryDataDirectories() const
+{
+ QMap<QString, QDir> directories;
+ directories.insert("game_data", gameDirectory().absoluteFilePath("Data"));
+ return directories;
+}
+
+QList<ExecutableInfo> GameStarfield::executables() const
+{
+ return QList<ExecutableInfo>()
+ << ExecutableInfo("SFSE",
+ findInGameFolder(m_Organizer->gameFeatures()
+ ->gameFeature<MOBase::ScriptExtender>()
+ ->loaderName()))
+ << ExecutableInfo("Starfield", findInGameFolder(binaryName()))
+ << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe"))
+ .withSteamAppId("2722710")
+ << ExecutableInfo("LOOT", QFileInfo(getLootPath()))
+ .withArgument("--game=\"Starfield\"");
+}
+
+QList<ExecutableForcedLoadSetting> GameStarfield::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+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<PluginSetting> GameStarfield::settings() const
+{
+ return QList<PluginSetting>()
+ << 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<const GamebryoSaveGame>
+GameStarfield::makeSaveGame(QString filePath) const
+{
+ return std::make_shared<const StarfieldSaveGame>(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<StarfieldUnmanagedMods> unmanagedMods =
+ std::static_pointer_cast<StarfieldUnmanagedMods>(
+ m_Organizer->gameFeatures()->gameFeature<MOBase::UnmanagedMods>());
+
+ // 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<unsigned int> GameStarfield::activeProblems() const
+{
+ std::vector<unsigned int> 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<QString> 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("<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>")
+ .arg(espInfo);
+ }
+ case PROBLEM_TEST_FILE: {
+ return tr("<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>");
+ }
+ }
+ 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 <QObject>
+#include <QtGlobal>
+
+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<QString, QDir> secondaryDataDirectories() const override;
+ virtual QList<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ 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<unsigned int> 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<MOBase::PluginSetting> settings() const override;
+ virtual MappingType mappings() const override;
+
+protected:
+ virtual QString identifyGamePath() const override;
+ std::shared_ptr<const GamebryoSaveGame> 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<QString> 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 <imoinfo.h>
+#include <utility.h>
+
+#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 <bsainvalidation.h>
+#include <dataarchives.h>
+
+#include <memory>
+
+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 <utility.h>
+
+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 <QDir>
+#include <QStringList>
+
+#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 <creationgameplugins.h>
+
+#include <QObject>
+#include <QtGlobal>
+
+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 <gamebryomoddatachecker.h>
+
+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 <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+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<Content> getAllContents() const override
+ {
+ static std::vector<Content> 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<int>
+ getContentsFor(std::shared_ptr<const MOBase::IFileTree> 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 <Windows.h>
+#else
+#include <QDateTime>
+
+using SYSTEMTIME = _SYSTEMTIME;
+
+static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st)
+{
+ const uint64_t ticks = (static_cast<uint64_t>(ft->dwHighDateTime) << 32) |
+ static_cast<uint64_t>(ft->dwLowDateTime);
+ const int64_t unixSecs = static_cast<int64_t>(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<uint16_t>(d.year());
+ st->wMonth = static_cast<uint16_t>(d.month());
+ st->wDayOfWeek = static_cast<uint16_t>(d.dayOfWeek() % 7);
+ st->wDay = static_cast<uint16_t>(d.day());
+ st->wHour = static_cast<uint16_t>(t.hour());
+ st->wMinute = static_cast<uint16_t>(t.minute());
+ st->wSecond = static_cast<uint16_t>(t.second());
+ st->wMilliseconds = static_cast<uint16_t>(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<uint32_t>(); // header version
+ file.skip<uint64_t>(); // chunk compressed size array start location
+ file.skip<uint64_t>(); // 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<unsigned short>(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<GamebryoSaveGame::DataFields> 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<DataFields> fields = std::make_unique<DataFields>();
+
+ 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 <QDataStream>
+
+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<DataFields> 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 <QString>
+#include <QStringList>
+
+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 <QFile>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonValue>
+
+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<QString, QDir> 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<QString, QDir> 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<QString, StarfieldUnmanagedMods::ContentCatalog>
+StarfieldUnmanagedMods::parseContentCatalog() const
+{
+ QFile content(m_AppDataFolder + "/" + game()->gameShortName() +
+ "/ContentCatalog.txt");
+ std::map<QString, StarfieldUnmanagedMods::ContentCatalog> 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<QString, QDir> 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 <gamegamebryo.h>
+
+#include <QJsonObject>
+
+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<QString, ContentCatalog> 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 <memory>
+
+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 <utility.h>
+
+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 <QDir>
+#include <QString>
+#include <QStringList>
+#include <gamebryodataarchives.h>
+#include <iprofile.h>
+
+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 <gamebryomoddatachecker.h>
+
+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 <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+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<uint32_t>(); // Save header size
+
+ file.skip<uint32_t>(); // File version?
+ file.skip<unsigned char>(); // 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<GamebryoSaveGame::DataFields>
+FalloutTTWSaveGame::fetchDataFields() const
+{
+ FileWrapper file(getFilepath(), "FO3SAVEGAME");
+
+ std::unique_ptr<DataFields> fields = std::make_unique<DataFields>();
+
+ 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<char>(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<DataFields> 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 <QString>
+#include <QStringList>
+
+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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameFalloutTTW</name>
+ <message>
+ <location filename="gamefalloutttw.cpp" line="185"/>
+ <source>Fallout TTW Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamefalloutttw.cpp" line="195"/>
+ <source>Adds support for the game Fallout TTW</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamefalloutttw.cpp" line="207"/>
+ <source>While not recommended by the TTW modding community, enables LOOT sorting</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <gamebryogameplugins.h>
+#include <gamebryolocalsavegames.h>
+#include <gamebryosavegameinfo.h>
+#include <gamebryounmanagedmods.h>
+
+#include <QCoreApplication>
+#include <QDir>
+#include <QFileInfo>
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <QStringList>
+
+#include <memory>
+
+using namespace MOBase;
+
+GameFalloutTTW::GameFalloutTTW() {}
+
+bool GameFalloutTTW::init(IOrganizer* moInfo)
+{
+ if (!GameGamebryo::init(moInfo)) {
+ return false;
+ }
+
+ auto dataArchives = std::make_shared<FalloutTTWDataArchives>(this);
+ registerFeature(std::make_shared<FalloutTTWScriptExtender>(this));
+ registerFeature(dataArchives);
+ registerFeature(
+ std::make_shared<FalloutTTWBSAInvalidation>(dataArchives.get(), this));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "fallout.ini"));
+ registerFeature(std::make_shared<FalloutTTWModDataChecker>(this));
+ registerFeature(
+ std::make_shared<FalloutTTWModDataContent>(m_Organizer->gameFeatures()));
+ registerFeature(std::make_shared<GamebryoGamePlugins>(moInfo));
+ registerFeature(std::make_shared<GamebryoUnmangedMods>(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<ExecutableInfo> GameFalloutTTW::executables() const
+{
+ ExecutableInfo game("Tale of Two Wastelands", findInGameFolder(binaryName()));
+ ExecutableInfo launcher("Fallout Launcher", findInGameFolder(getLauncherName()));
+ QList<ExecutableInfo> extraExecutables =
+ QList<ExecutableInfo>() << 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<MOBase::ScriptExtender>()
+ ->loaderName())));
+ } else {
+ game.withArgument("-EpicPortal");
+ launcher.withArgument("-EpicPortal");
+ }
+ QList<ExecutableInfo> executables = {game, launcher};
+ executables += extraExecutables;
+ return executables;
+}
+
+QList<ExecutableForcedLoadSetting> GameFalloutTTW::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+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<PluginSetting> GameFalloutTTW::settings() const
+{
+ return QList<PluginSetting>()
+ << 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<const GamebryoSaveGame>
+GameFalloutTTW::makeSaveGame(QString filePath) const
+{
+ return std::make_shared<const FalloutTTWSaveGame>(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 <QObject>
+#include <QtGlobal>
+
+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<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ 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<MOBase::PluginSetting> 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<const GamebryoSaveGame> 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 @@
+{}
diff --git a/libs/game_bethesda/vcpkg.json b/libs/game_bethesda/vcpkg.json
new file mode 100644
index 0000000..2ee9a55
--- /dev/null
+++ b/libs/game_bethesda/vcpkg.json
@@ -0,0 +1,31 @@
+{
+ "dependencies": [
+ "lz4",
+ "zlib"
+ ],
+ "features": {
+ "standalone": {
+ "description": "Build Standalone.",
+ "dependencies": [
+ "mo2-cmake"
+ ]
+ }
+ },
+ "vcpkg-configuration": {
+ "default-registry": {
+ "kind": "git",
+ "repository": "https://github.com/Microsoft/vcpkg",
+ "baseline": "294f76666c3000630d828703e675814c05a4fd43"
+ },
+ "registries": [
+ {
+ "kind": "git",
+ "repository": "https://github.com/ModOrganizer2/vcpkg-registry",
+ "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673",
+ "packages": [
+ "mo2-*"
+ ]
+ }
+ ]
+ }
+}