aboutsummaryrefslogtreecommitdiff
path: root/libs/game_bethesda/src/games
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/src/games
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/src/games')
-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
282 files changed, 14392 insertions, 0 deletions
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 @@
+{}